mirror of https://github.com/Radarr/Radarr
Compare commits
86 Commits
v5.4.5.871
...
develop
Author | SHA1 | Date |
---|---|---|
Servarr | 98384ab390 | |
Bogdan | 0c654377f4 | |
Bogdan | e8c925274a | |
Bogdan | 320bfeec16 | |
Bogdan | 638f92495c | |
Bogdan | 077b041d3f | |
Bogdan | ff3dd3ae42 | |
Bogdan | 2e3beddcbc | |
Servarr | dc068bbf3d | |
Bogdan | 7a303c1ebf | |
Bogdan | 152f50a1ef | |
Bogdan | 9798202589 | |
Bogdan | 7969776339 | |
Bogdan | 288982d7bd | |
Servarr | d39a3ade5b | |
Bogdan | 1fc6e88bc4 | |
Bogdan | e8e1841e6c | |
Bogdan | d17eb4f33f | |
Bogdan | 685f462959 | |
Servarr | 7be8a34130 | |
Ivan Sanz Carasa | 886711b496 | |
Servarr | 5185e037da | |
Mark McDowall | 38e7e37d57 | |
Stevie Robinson | 190c4c5893 | |
Mark McDowall | 0ec18ce4b3 | |
Weblate | a08575b7bc | |
Bogdan | 556cc885ec | |
Bogdan | 586c0c6e13 | |
Bogdan | cec569461d | |
Mark McDowall | 8b79b5afbf | |
Mickaël Thomas | cd4552ce6f | |
Bogdan | 256439304b | |
Bogdan | bb44fbc362 | |
Servarr | cd401f72f5 | |
Bogdan | c9624e7550 | |
Bogdan | 649702eaca | |
Servarr | 1c52f0f5bd | |
Bogdan | dff85dc1f3 | |
Bogdan | 1090aeff75 | |
Jared | 086a0addba | |
Bogdan | 8b6cf34ce4 | |
Jared | 7f03a916f1 | |
Mika | 3a6d603a9e | |
Weblate | cd2c7dc7fb | |
Bogdan | f1d76c3483 | |
Stevie Robinson | 39eac4b5ad | |
Mark McDowall | 71e1003358 | |
Bogdan | 89b6a5d51f | |
Bogdan | 711637c448 | |
Bruno Garcia | 2677d25980 | |
Bogdan | 56639bcd42 | |
Bogdan | 1ed62b9ced | |
Servarr | a596dda253 | |
Bogdan | c0b354039d | |
Bogdan | 3b5078d117 | |
Bogdan | db1fee8d8a | |
Mark McDowall | 0d0575f3a9 | |
Stevie Robinson | 2d82347a66 | |
Bogdan | 25838df550 | |
Mark McDowall | b3a8b99f9a | |
Christopher | 93a852841f | |
Bogdan | ead1ec43be | |
Weblate | 04b6dd44cb | |
Bogdan | 3db78079f3 | |
Bogdan | c8a6b9f565 | |
Bogdan | 811cafd9ae | |
fireph | ac7039d651 | |
Bogdan | a2d11cf684 | |
Bogdan | cc32635f6f | |
Bogdan | 10f9cb64ac | |
Weblate | f77e27bace | |
Servarr | 8ea6d59d59 | |
Bogdan | 98668d0d25 | |
Gauthier | 649d57a234 | |
Josh McKinney | dc7c8bf800 | |
Bogdan | 8d90c7678f | |
Bogdan | 02518e2116 | |
Mark McDowall | 3191a883dc | |
Bogdan | 31a714e6b3 | |
Mark McDowall | f7ca0b8b06 | |
Josh McKinney | 56be9502af | |
Mark McDowall | 77381d3f72 | |
Bogdan | 198e6324e0 | |
Alan Collins | 81c9537e5a | |
Bogdan | d3cbb9be8d | |
Bogdan | 2e043c0cf7 |
|
@ -0,0 +1,13 @@
|
||||||
|
// This file is used to open the backend and frontend in the same workspace, which is necessary as
|
||||||
|
// the frontend has vscode settings that are distinct from the backend
|
||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": ".."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../frontend"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
|
||||||
|
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||||
|
{
|
||||||
|
"name": "Radarr",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
|
"nodeGypDependencies": true,
|
||||||
|
"version": "16",
|
||||||
|
"nvmVersion": "latest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"forwardPorts": [7878],
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": ["esbenp.prettier-vscode"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
# To get started with Dependabot version updates, you'll need to specify which
|
||||||
|
# package ecosystems to update and where the package manifests are located.
|
||||||
|
# Please see the documentation for more information:
|
||||||
|
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||||
|
# https://containers.dev/guide/dependabot
|
||||||
|
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "devcontainers"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
|
@ -126,6 +126,7 @@ coverage*.xml
|
||||||
coverage*.json
|
coverage*.json
|
||||||
setup/Output/
|
setup/Output/
|
||||||
*.~is
|
*.~is
|
||||||
|
.mono
|
||||||
|
|
||||||
# VS outout folders
|
# VS outout folders
|
||||||
bin
|
bin
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"ms-dotnettools.csdevkit",
|
||||||
|
"ms-vscode-remote.remote-containers"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||||
|
// Use hover for the description of the existing attributes
|
||||||
|
// For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md
|
||||||
|
"name": "Run Radarr",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "launch",
|
||||||
|
"preLaunchTask": "build dotnet",
|
||||||
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
|
"program": "${workspaceFolder}/_output/net6.0/Radarr",
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||||
|
"console": "integratedTerminal",
|
||||||
|
"stopAtEntry": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": ".NET Core Attach",
|
||||||
|
"type": "coreclr",
|
||||||
|
"request": "attach"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build dotnet",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"msbuild",
|
||||||
|
"-restore",
|
||||||
|
"${workspaceFolder}/src/Radarr.sln",
|
||||||
|
"-p:GenerateFullPaths=true",
|
||||||
|
"-p:Configuration=Debug",
|
||||||
|
"-p:Platform=Posix",
|
||||||
|
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "publish",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"publish",
|
||||||
|
"${workspaceFolder}/src/Radarr.sln",
|
||||||
|
"-property:GenerateFullPaths=true",
|
||||||
|
"-consoleloggerparameters:NoSummary;ForceNoAlign"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "watch",
|
||||||
|
"command": "dotnet",
|
||||||
|
"type": "process",
|
||||||
|
"args": [
|
||||||
|
"watch",
|
||||||
|
"run",
|
||||||
|
"--project",
|
||||||
|
"${workspaceFolder}/src/Radarr.sln"
|
||||||
|
],
|
||||||
|
"problemMatcher": "$msCompile"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -9,13 +9,13 @@ variables:
|
||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '5.4.5'
|
majorVersion: '5.7.0'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.417'
|
dotnetVersion: '6.0.421'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.2'
|
innoVersion: '6.2.2'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
|
@ -166,10 +166,10 @@ stages:
|
||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: UseNode@1
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(nodeVersion)
|
version: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
|
@ -1089,10 +1089,10 @@ stages:
|
||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
steps:
|
steps:
|
||||||
- task: NodeTool@0
|
- task: UseNode@1
|
||||||
displayName: Set Node.js version
|
displayName: Set Node.js version
|
||||||
inputs:
|
inputs:
|
||||||
versionSpec: $(nodeVersion)
|
version: $(nodeVersion)
|
||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
|
|
|
@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
@ -20,6 +21,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
|
import BlocklistFilterModal from './BlocklistFilterModal';
|
||||||
import BlocklistRowConnector from './BlocklistRowConnector';
|
import BlocklistRowConnector from './BlocklistRowConnector';
|
||||||
|
|
||||||
class Blocklist extends Component {
|
class Blocklist extends Component {
|
||||||
|
@ -114,9 +116,13 @@ class Blocklist extends Component {
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
columns,
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
isRemoving,
|
isRemoving,
|
||||||
isClearingBlocklistExecuting,
|
isClearingBlocklistExecuting,
|
||||||
|
onFilterSelect,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -161,6 +167,15 @@ class Blocklist extends Component {
|
||||||
iconName={icons.TABLE}
|
iconName={icons.TABLE}
|
||||||
/>
|
/>
|
||||||
</TableOptionsModalWrapper>
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={BlocklistFilterModal}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
|
@ -180,7 +195,11 @@ class Blocklist extends Component {
|
||||||
{
|
{
|
||||||
isPopulated && !error && !items.length &&
|
isPopulated && !error && !items.length &&
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
{translate('NoHistoryBlocklist')}
|
{
|
||||||
|
selectedFilterKey === 'all' ?
|
||||||
|
translate('NoHistoryBlocklist') :
|
||||||
|
translate('BlocklistFilterHasNoItems')
|
||||||
|
}
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,11 +270,15 @@ Blocklist.propTypes = {
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: 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,
|
||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
isRemoving: PropTypes.bool.isRequired,
|
||||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||||
onRemoveSelected: PropTypes.func.isRequired,
|
onRemoveSelected: PropTypes.func.isRequired,
|
||||||
onClearBlocklistPress: PropTypes.func.isRequired
|
onClearBlocklistPress: PropTypes.func.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Blocklist;
|
export default Blocklist;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import * as blocklistActions from 'Store/Actions/blocklistActions';
|
import * as blocklistActions from 'Store/Actions/blocklistActions';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
import Blocklist from './Blocklist';
|
import Blocklist from './Blocklist';
|
||||||
|
@ -13,10 +14,12 @@ import Blocklist from './Blocklist';
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.blocklist,
|
(state) => state.blocklist,
|
||||||
|
createCustomFiltersSelector('blocklist'),
|
||||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
||||||
(blocklist, isClearingBlocklistExecuting) => {
|
(blocklist, customFilters, isClearingBlocklistExecuting) => {
|
||||||
return {
|
return {
|
||||||
isClearingBlocklistExecuting,
|
isClearingBlocklistExecuting,
|
||||||
|
customFilters,
|
||||||
...blocklist
|
...blocklist
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -97,6 +100,14 @@ class BlocklistConnector extends Component {
|
||||||
this.props.setBlocklistSort({ sortKey });
|
this.props.setBlocklistSort({ sortKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
|
this.props.setBlocklistFilter({ selectedFilterKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
onClearBlocklistPress = () => {
|
||||||
|
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
||||||
|
};
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
onTableOptionChange = (payload) => {
|
||||||
this.props.setBlocklistTableOption(payload);
|
this.props.setBlocklistTableOption(payload);
|
||||||
|
|
||||||
|
@ -105,10 +116,6 @@ class BlocklistConnector extends Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onClearBlocklistPress = () => {
|
|
||||||
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
@ -122,6 +129,7 @@ class BlocklistConnector extends Component {
|
||||||
onPageSelect={this.onPageSelect}
|
onPageSelect={this.onPageSelect}
|
||||||
onRemoveSelected={this.onRemoveSelected}
|
onRemoveSelected={this.onRemoveSelected}
|
||||||
onSortPress={this.onSortPress}
|
onSortPress={this.onSortPress}
|
||||||
|
onFilterSelect={this.onFilterSelect}
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
onClearBlocklistPress={this.onClearBlocklistPress}
|
onClearBlocklistPress={this.onClearBlocklistPress}
|
||||||
{...this.props}
|
{...this.props}
|
||||||
|
@ -142,6 +150,7 @@ BlocklistConnector.propTypes = {
|
||||||
gotoBlocklistPage: PropTypes.func.isRequired,
|
gotoBlocklistPage: PropTypes.func.isRequired,
|
||||||
removeBlocklistItems: PropTypes.func.isRequired,
|
removeBlocklistItems: PropTypes.func.isRequired,
|
||||||
setBlocklistSort: PropTypes.func.isRequired,
|
setBlocklistSort: PropTypes.func.isRequired,
|
||||||
|
setBlocklistFilter: PropTypes.func.isRequired,
|
||||||
setBlocklistTableOption: PropTypes.func.isRequired,
|
setBlocklistTableOption: PropTypes.func.isRequired,
|
||||||
clearBlocklist: PropTypes.func.isRequired,
|
clearBlocklist: PropTypes.func.isRequired,
|
||||||
executeCommand: PropTypes.func.isRequired
|
executeCommand: PropTypes.func.isRequired
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
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 { setBlocklistFilter } from 'Store/Actions/blocklistActions';
|
||||||
|
|
||||||
|
function createBlocklistSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.blocklist.items,
|
||||||
|
(blocklistItems) => {
|
||||||
|
return blocklistItems;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.blocklist.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BlocklistFilterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function BlocklistFilterModal(props: BlocklistFilterModalProps) {
|
||||||
|
const sectionItems = useSelector(createBlocklistSelector());
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'blocklist';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload: unknown) => {
|
||||||
|
dispatch(setBlocklistFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
// TODO: Don't spread all the props
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -12,11 +12,10 @@ function App({ store, history }) {
|
||||||
<DocumentTitle title={window.Radarr.instanceName}>
|
<DocumentTitle title={window.Radarr.instanceName}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<ApplyTheme>
|
<ApplyTheme />
|
||||||
<PageConnector>
|
<PageConnector>
|
||||||
<AppRoutes app={App} />
|
<AppRoutes app={App} />
|
||||||
</PageConnector>
|
</PageConnector>
|
||||||
</ApplyTheme>
|
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
</DocumentTitle>
|
</DocumentTitle>
|
||||||
|
|
|
@ -33,6 +33,8 @@ import Status from 'System/Status/Status';
|
||||||
import Tasks from 'System/Tasks/Tasks';
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
|
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||||
|
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
||||||
|
|
||||||
function AppRoutes(props) {
|
function AppRoutes(props) {
|
||||||
const {
|
const {
|
||||||
|
@ -121,6 +123,20 @@ function AppRoutes(props) {
|
||||||
component={BlocklistConnector}
|
component={BlocklistConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Wanted
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/wanted/missing"
|
||||||
|
component={MissingConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/wanted/cutoffunmet"
|
||||||
|
component={CutoffUnmetConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
Settings
|
Settings
|
||||||
*/}
|
*/}
|
||||||
|
|
|
@ -1,49 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Fragment, useCallback, useEffect } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import themes from 'Styles/Themes';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.ui.item.theme || window.Radarr.theme,
|
|
||||||
(
|
|
||||||
theme
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
theme
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ApplyTheme({ theme, children }) {
|
|
||||||
// Update the CSS Variables
|
|
||||||
const updateCSSVariables = useCallback(() => {
|
|
||||||
const arrayOfVariableKeys = Object.keys(themes[theme]);
|
|
||||||
const arrayOfVariableValues = Object.values(themes[theme]);
|
|
||||||
|
|
||||||
// Loop through each array key and set the CSS Variables
|
|
||||||
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
|
|
||||||
// Based on our snippet from MDN
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
`--${cssVariableKey}`,
|
|
||||||
arrayOfVariableValues[index]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
// On Component Mount and Component Update
|
|
||||||
useEffect(() => {
|
|
||||||
updateCSSVariables(theme);
|
|
||||||
}, [updateCSSVariables, theme]);
|
|
||||||
|
|
||||||
return <Fragment>{children}</Fragment>;
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplyTheme.propTypes = {
|
|
||||||
theme: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(ApplyTheme);
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import React, { Fragment, ReactNode, useCallback, useEffect } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import themes from 'Styles/Themes';
|
||||||
|
import AppState from './State/AppState';
|
||||||
|
|
||||||
|
interface ApplyThemeProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createThemeSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.ui.item.theme || window.Radarr.theme,
|
||||||
|
(theme) => {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ApplyTheme({ children }: ApplyThemeProps) {
|
||||||
|
const theme = useSelector(createThemeSelector());
|
||||||
|
|
||||||
|
const updateCSSVariables = useCallback(() => {
|
||||||
|
Object.entries(themes[theme]).forEach(([key, value]) => {
|
||||||
|
document.documentElement.style.setProperty(`--${key}`, value);
|
||||||
|
});
|
||||||
|
}, [theme]);
|
||||||
|
|
||||||
|
// On Component Mount and Component Update
|
||||||
|
useEffect(() => {
|
||||||
|
updateCSSVariables();
|
||||||
|
}, [updateCSSVariables, theme]);
|
||||||
|
|
||||||
|
return <Fragment>{children}</Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ApplyTheme;
|
|
@ -1,4 +1,5 @@
|
||||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||||
|
import BlocklistAppState from './BlocklistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import HistoryAppState from './HistoryAppState';
|
import HistoryAppState from './HistoryAppState';
|
||||||
|
@ -54,6 +55,7 @@ export interface AppSectionState {
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
app: AppSectionState;
|
app: AppSectionState;
|
||||||
|
blocklist: BlocklistAppState;
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import Blocklist from 'typings/Blocklist';
|
||||||
|
import AppSectionState, { AppSectionFilterState } from './AppSectionState';
|
||||||
|
|
||||||
|
interface BlocklistAppState
|
||||||
|
extends AppSectionState<Blocklist>,
|
||||||
|
AppSectionFilterState<Blocklist> {}
|
||||||
|
|
||||||
|
export default BlocklistAppState;
|
|
@ -115,3 +115,16 @@ $hoverScale: 1.05;
|
||||||
color: var(--iconButtonHoverLightColor);
|
color: var(--iconButtonHoverLightColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.excluded {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-width: 0 25px 25px 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent var(--dangerColor) transparent transparent;
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ interface CssExports {
|
||||||
'content': string;
|
'content': string;
|
||||||
'controls': string;
|
'controls': string;
|
||||||
'editorSelect': string;
|
'editorSelect': string;
|
||||||
|
'excluded': string;
|
||||||
'externalLinks': string;
|
'externalLinks': string;
|
||||||
'link': string;
|
'link': string;
|
||||||
'monitorToggleButton': string;
|
'monitorToggleButton': string;
|
||||||
|
|
|
@ -5,6 +5,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import AddNewCollectionMovieModal from './../AddNewCollectionMovieModal';
|
import AddNewCollectionMovieModal from './../AddNewCollectionMovieModal';
|
||||||
import styles from './CollectionMovie.css';
|
import styles from './CollectionMovie.css';
|
||||||
|
|
||||||
|
@ -72,6 +73,7 @@ class CollectionMovie extends Component {
|
||||||
isAvailable,
|
isAvailable,
|
||||||
movieFile,
|
movieFile,
|
||||||
isExistingMovie,
|
isExistingMovie,
|
||||||
|
isExcluded,
|
||||||
posterWidth,
|
posterWidth,
|
||||||
posterHeight,
|
posterHeight,
|
||||||
detailedProgressBar,
|
detailedProgressBar,
|
||||||
|
@ -107,6 +109,15 @@ class CollectionMovie extends Component {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isExcluded ?
|
||||||
|
<div
|
||||||
|
className={styles.excluded}
|
||||||
|
title={translate('Excluded')}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
className={styles.link}
|
className={styles.link}
|
||||||
style={elementStyle}
|
style={elementStyle}
|
||||||
|
@ -189,6 +200,7 @@ CollectionMovie.propTypes = {
|
||||||
posterHeight: PropTypes.number.isRequired,
|
posterHeight: PropTypes.number.isRequired,
|
||||||
detailedProgressBar: PropTypes.bool.isRequired,
|
detailedProgressBar: PropTypes.bool.isRequired,
|
||||||
isExistingMovie: PropTypes.bool,
|
isExistingMovie: PropTypes.bool,
|
||||||
|
isExcluded: PropTypes.bool,
|
||||||
tmdbId: PropTypes.number.isRequired,
|
tmdbId: PropTypes.number.isRequired,
|
||||||
imdbId: PropTypes.string,
|
imdbId: PropTypes.string,
|
||||||
youTubeTrailerId: PropTypes.string,
|
youTubeTrailerId: PropTypes.string,
|
||||||
|
|
|
@ -17,6 +17,7 @@ import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
|
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
|
||||||
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
|
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
|
||||||
|
import MovieTagInput from './MovieTagInput';
|
||||||
import NumberInput from './NumberInput';
|
import NumberInput from './NumberInput';
|
||||||
import OAuthInputConnector from './OAuthInputConnector';
|
import OAuthInputConnector from './OAuthInputConnector';
|
||||||
import PasswordInput from './PasswordInput';
|
import PasswordInput from './PasswordInput';
|
||||||
|
@ -89,6 +90,10 @@ function getComponent(type) {
|
||||||
|
|
||||||
case inputTypes.DYNAMIC_SELECT:
|
case inputTypes.DYNAMIC_SELECT:
|
||||||
return EnhancedSelectInputConnector;
|
return EnhancedSelectInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.MOVIE_TAG:
|
||||||
|
return MovieTagInput;
|
||||||
|
|
||||||
case inputTypes.TAG:
|
case inputTypes.TAG:
|
||||||
return TagInputConnector;
|
return TagInputConnector;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import TagInputConnector from './TagInputConnector';
|
||||||
|
|
||||||
|
interface MovieTagInputProps {
|
||||||
|
name: string;
|
||||||
|
value: number | number[];
|
||||||
|
onChange: ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
value: number | number[];
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function MovieTagInput(props: MovieTagInputProps) {
|
||||||
|
const { value, onChange, ...otherProps } = props;
|
||||||
|
const isArray = Array.isArray(value);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
({ name, value: newValue }: { name: string; value: number[] }) => {
|
||||||
|
if (isArray) {
|
||||||
|
onChange({ name, value: newValue });
|
||||||
|
} else {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: newValue.length ? newValue[newValue.length - 1] : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isArray, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
let finalValue: number[] = [];
|
||||||
|
|
||||||
|
if (isArray) {
|
||||||
|
finalValue = value;
|
||||||
|
} else if (value === 0) {
|
||||||
|
finalValue = [];
|
||||||
|
} else {
|
||||||
|
finalValue = [value];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore 2786 'TagInputConnector' isn't typed yet
|
||||||
|
<TagInputConnector
|
||||||
|
{...otherProps}
|
||||||
|
value={finalValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,5 +0,0 @@
|
||||||
.input {
|
|
||||||
composes: input from '~Components/Form/TextInput.css';
|
|
||||||
|
|
||||||
font-family: $passwordFamily;
|
|
||||||
}
|
|
|
@ -1,7 +1,5 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
import styles from './PasswordInput.css';
|
|
||||||
|
|
||||||
// Prevent a user from copying (or cutting) the password from the input
|
// Prevent a user from copying (or cutting) the password from the input
|
||||||
function onCopy(e) {
|
function onCopy(e) {
|
||||||
|
@ -13,17 +11,14 @@ function PasswordInput(props) {
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
{...props}
|
{...props}
|
||||||
|
type="password"
|
||||||
onCopy={onCopy}
|
onCopy={onCopy}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
PasswordInput.propTypes = {
|
PasswordInput.propTypes = {
|
||||||
className: PropTypes.string.isRequired
|
...TextInput.props
|
||||||
};
|
|
||||||
|
|
||||||
PasswordInput.defaultProps = {
|
|
||||||
className: styles.input
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PasswordInput;
|
export default PasswordInput;
|
||||||
|
|
|
@ -27,6 +27,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
||||||
return inputTypes.DYNAMIC_SELECT;
|
return inputTypes.DYNAMIC_SELECT;
|
||||||
}
|
}
|
||||||
return inputTypes.SELECT;
|
return inputTypes.SELECT;
|
||||||
|
case 'movieTag':
|
||||||
|
return inputTypes.MOVIE_TAG;
|
||||||
case 'tag':
|
case 'tag':
|
||||||
return inputTypes.TEXT_TAG;
|
return inputTypes.TEXT_TAG;
|
||||||
case 'tagSelect':
|
case 'tagSelect':
|
||||||
|
|
|
@ -71,6 +71,22 @@ const links = [
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
iconName: icons.WARNING,
|
||||||
|
title: () => translate('Wanted'),
|
||||||
|
to: '/wanted/missing',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: () => translate('Missing'),
|
||||||
|
to: '/wanted/missing'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: () => translate('CutoffUnmet'),
|
||||||
|
to: '/wanted/cutoffunmet'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
iconName: icons.SETTINGS,
|
iconName: icons.SETTINGS,
|
||||||
title: () => translate('Settings'),
|
title: () => translate('Settings'),
|
||||||
|
|
|
@ -244,6 +244,26 @@ class SignalRConnector extends Component {
|
||||||
this.props.dispatchSetVersion({ version });
|
this.props.dispatchSetVersion({ version });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
handleWantedCutoff = (body) => {
|
||||||
|
if (body.action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({
|
||||||
|
section: 'wanted.cutoffUnmet',
|
||||||
|
updateOnly: true,
|
||||||
|
...body.resource
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
handleWantedMissing = (body) => {
|
||||||
|
if (body.action === 'updated') {
|
||||||
|
this.props.dispatchUpdateItem({
|
||||||
|
section: 'wanted.missing',
|
||||||
|
updateOnly: true,
|
||||||
|
...body.resource
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
handleSystemTask = () => {
|
handleSystemTask = () => {
|
||||||
this.props.dispatchFetchCommands();
|
this.props.dispatchFetchCommands();
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,14 +25,3 @@
|
||||||
font-family: 'Ubuntu Mono';
|
font-family: 'Ubuntu Mono';
|
||||||
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
src: url('UbuntuMono-Regular.eot?#iefix&v=1.3.0') format('embedded-opentype'), url('UbuntuMono-Regular.woff?v=1.3.0') format('woff'), url('UbuntuMono-Regular.ttf?v=1.3.0') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* text-security-disc
|
|
||||||
*/
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-weight: normal;
|
|
||||||
font-style: normal;
|
|
||||||
font-family: 'text-security-disc';
|
|
||||||
src: url('text-security-disc.woff?v=1.3.0') format('woff'), url('text-security-disc.ttf?v=1.3.0') format('truetype');
|
|
||||||
}
|
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -75,9 +75,19 @@ class DiscoverMovie extends Component {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection
|
sortDirection,
|
||||||
|
includeRecommendations,
|
||||||
|
includeTrending,
|
||||||
|
includePopular
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
if (includeRecommendations !== prevProps.includeRecommendations ||
|
||||||
|
includeTrending !== prevProps.includeTrending ||
|
||||||
|
includePopular !== prevProps.includePopular
|
||||||
|
) {
|
||||||
|
this.props.dispatchFetchListMovies();
|
||||||
|
}
|
||||||
|
|
||||||
if (sortKey !== prevProps.sortKey ||
|
if (sortKey !== prevProps.sortKey ||
|
||||||
sortDirection !== prevProps.sortDirection ||
|
sortDirection !== prevProps.sortDirection ||
|
||||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||||
|
@ -443,6 +453,9 @@ DiscoverMovie.propTypes = {
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
view: PropTypes.string.isRequired,
|
view: PropTypes.string.isRequired,
|
||||||
|
includeRecommendations: PropTypes.bool.isRequired,
|
||||||
|
includeTrending: PropTypes.bool.isRequired,
|
||||||
|
includePopular: PropTypes.bool.isRequired,
|
||||||
isSyncingLists: PropTypes.bool.isRequired,
|
isSyncingLists: PropTypes.bool.isRequired,
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
onSortSelect: PropTypes.func.isRequired,
|
onSortSelect: PropTypes.func.isRequired,
|
||||||
|
@ -451,7 +464,8 @@ DiscoverMovie.propTypes = {
|
||||||
onScroll: PropTypes.func.isRequired,
|
onScroll: PropTypes.func.isRequired,
|
||||||
onAddMoviesPress: PropTypes.func.isRequired,
|
onAddMoviesPress: PropTypes.func.isRequired,
|
||||||
onExcludeMoviesPress: PropTypes.func.isRequired,
|
onExcludeMoviesPress: PropTypes.func.isRequired,
|
||||||
onImportListSyncPress: PropTypes.func.isRequired
|
onImportListSyncPress: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchListMovies: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default DiscoverMovie;
|
export default DiscoverMovie;
|
||||||
|
|
|
@ -17,15 +17,18 @@ import DiscoverMovie from './DiscoverMovie';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
(state) => state.discoverMovie,
|
||||||
createDiscoverMovieClientSideCollectionItemsSelector('discoverMovie'),
|
createDiscoverMovieClientSideCollectionItemsSelector('discoverMovie'),
|
||||||
createCommandExecutingSelector(commandNames.IMPORT_LIST_SYNC),
|
createCommandExecutingSelector(commandNames.IMPORT_LIST_SYNC),
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(
|
(
|
||||||
|
discoverMovie,
|
||||||
movies,
|
movies,
|
||||||
isSyncingLists,
|
isSyncingLists,
|
||||||
dimensionsState
|
dimensionsState
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
|
...discoverMovie.options,
|
||||||
...movies,
|
...movies,
|
||||||
isSyncingLists,
|
isSyncingLists,
|
||||||
isSmallScreen: dimensionsState.isSmallScreen
|
isSmallScreen: dimensionsState.isSmallScreen
|
||||||
|
|
|
@ -49,7 +49,9 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||||
showRatings: props.showRatings,
|
showRatings: props.showRatings,
|
||||||
showYear: props.showYear,
|
showYear: props.showYear,
|
||||||
showGenres: props.showGenres,
|
showGenres: props.showGenres,
|
||||||
includeRecommendations: props.includeRecommendations
|
includeRecommendations: props.includeRecommendations,
|
||||||
|
includeTrending: props.includeTrending,
|
||||||
|
includePopular: props.includePopular
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +63,9 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||||
showRatings,
|
showRatings,
|
||||||
showCertification,
|
showCertification,
|
||||||
showGenres,
|
showGenres,
|
||||||
includeRecommendations
|
includeRecommendations,
|
||||||
|
includeTrending,
|
||||||
|
includePopular
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const state = {};
|
const state = {};
|
||||||
|
@ -94,6 +98,14 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||||
state.includeRecommendations = includeRecommendations;
|
state.includeRecommendations = includeRecommendations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (includeTrending !== prevProps.includeTrending) {
|
||||||
|
state.includeTrending = includeTrending;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includePopular !== prevProps.includePopular) {
|
||||||
|
state.includePopular = includePopular;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_.isEmpty(state)) {
|
if (!_.isEmpty(state)) {
|
||||||
this.setState(state);
|
this.setState(state);
|
||||||
}
|
}
|
||||||
|
@ -135,19 +147,22 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||||
showRatings,
|
showRatings,
|
||||||
showYear,
|
showYear,
|
||||||
showGenres,
|
showGenres,
|
||||||
includeRecommendations
|
includeRecommendations,
|
||||||
|
includeTrending,
|
||||||
|
includePopular
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Overview Options
|
{translate('OverviewOptions')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Form>
|
<Form>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('IncludeRadarrRecommendations')}</FormLabel>
|
<FormLabel>{translate('IncludeRadarrRecommendations')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="includeRecommendations"
|
name="includeRecommendations"
|
||||||
|
@ -157,6 +172,30 @@ class DiscoverMovieOverviewOptionsModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludeTrending')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includeTrending"
|
||||||
|
value={includeTrending}
|
||||||
|
helpText={translate('IncludeTrendingMoviesHelpText')}
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludePopular')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includePopular"
|
||||||
|
value={includePopular}
|
||||||
|
helpText={translate('IncludePopularMoviesHelpText')}
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('PosterSize')}</FormLabel>
|
<FormLabel>{translate('PosterSize')}</FormLabel>
|
||||||
|
|
||||||
|
@ -246,6 +285,8 @@ DiscoverMovieOverviewOptionsModalContent.propTypes = {
|
||||||
showCertification: PropTypes.bool.isRequired,
|
showCertification: PropTypes.bool.isRequired,
|
||||||
showGenres: PropTypes.bool.isRequired,
|
showGenres: PropTypes.bool.isRequired,
|
||||||
includeRecommendations: PropTypes.bool.isRequired,
|
includeRecommendations: PropTypes.bool.isRequired,
|
||||||
|
includeTrending: PropTypes.bool.isRequired,
|
||||||
|
includePopular: PropTypes.bool.isRequired,
|
||||||
onChangeOverviewOption: PropTypes.func.isRequired,
|
onChangeOverviewOption: PropTypes.func.isRequired,
|
||||||
onChangeOption: PropTypes.func.isRequired,
|
onChangeOption: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
|
|
@ -3,7 +3,7 @@ import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import TmdbRating from 'Components/TmdbRating';
|
import TmdbRating from 'Components/TmdbRating';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import { getMovieStatusDetails } from 'Movie/MovieStatus';
|
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
|
@ -45,7 +45,9 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
size: props.size,
|
size: props.size,
|
||||||
showTitle: props.showTitle,
|
showTitle: props.showTitle,
|
||||||
includeRecommendations: props.includeRecommendations
|
includeRecommendations: props.includeRecommendations,
|
||||||
|
includeTrending: props.includeTrending,
|
||||||
|
includePopular: props.includePopular
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +55,9 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||||
const {
|
const {
|
||||||
size,
|
size,
|
||||||
showTitle,
|
showTitle,
|
||||||
includeRecommendations
|
includeRecommendations,
|
||||||
|
includeTrending,
|
||||||
|
includePopular
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const state = {};
|
const state = {};
|
||||||
|
@ -70,6 +74,14 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||||
state.includeRecommendations = includeRecommendations;
|
state.includeRecommendations = includeRecommendations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (includeTrending !== prevProps.includeTrending) {
|
||||||
|
state.includeTrending = includeTrending;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includePopular !== prevProps.includePopular) {
|
||||||
|
state.includePopular = includePopular;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_.isEmpty(state)) {
|
if (!_.isEmpty(state)) {
|
||||||
this.setState(state);
|
this.setState(state);
|
||||||
}
|
}
|
||||||
|
@ -107,13 +119,15 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||||
const {
|
const {
|
||||||
size,
|
size,
|
||||||
showTitle,
|
showTitle,
|
||||||
includeRecommendations
|
includeRecommendations,
|
||||||
|
includeTrending,
|
||||||
|
includePopular
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Poster Options
|
{translate('PosterOptions')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
|
@ -130,6 +144,30 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludeTrending')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includeTrending"
|
||||||
|
value={includeTrending}
|
||||||
|
helpText={translate('IncludeTrendingMoviesHelpText')}
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludePopular')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includePopular"
|
||||||
|
value={includePopular}
|
||||||
|
helpText={translate('IncludePopularMoviesHelpText')}
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('PosterSize')}</FormLabel>
|
<FormLabel>{translate('PosterSize')}</FormLabel>
|
||||||
|
|
||||||
|
@ -172,6 +210,8 @@ DiscoverMoviePosterOptionsModalContent.propTypes = {
|
||||||
size: PropTypes.string.isRequired,
|
size: PropTypes.string.isRequired,
|
||||||
showTitle: PropTypes.bool.isRequired,
|
showTitle: PropTypes.bool.isRequired,
|
||||||
includeRecommendations: PropTypes.bool.isRequired,
|
includeRecommendations: PropTypes.bool.isRequired,
|
||||||
|
includeTrending: PropTypes.bool.isRequired,
|
||||||
|
includePopular: PropTypes.bool.isRequired,
|
||||||
onChangePosterOption: PropTypes.func.isRequired,
|
onChangePosterOption: PropTypes.func.isRequired,
|
||||||
onChangeOption: PropTypes.func.isRequired,
|
onChangeOption: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
|
|
@ -15,17 +15,29 @@ class DiscoverMovieTableOptions extends Component {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
includeRecommendations: props.includeRecommendations
|
includeRecommendations: props.includeRecommendations,
|
||||||
|
includeTrending: props.includeTrending,
|
||||||
|
includePopular: props.includePopular
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { includeRecommendations } = this.props;
|
const {
|
||||||
|
includeRecommendations,
|
||||||
|
includeTrending,
|
||||||
|
includePopular
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
if (includeRecommendations !== prevProps.includeRecommendations) {
|
if (includeRecommendations !== prevProps.includeRecommendations) {
|
||||||
this.setState({
|
this.setState({ includeRecommendations });
|
||||||
includeRecommendations
|
}
|
||||||
});
|
|
||||||
|
if (includeTrending !== prevProps.includeTrending) {
|
||||||
|
this.setState({ includeTrending });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includePopular !== prevProps.includePopular) {
|
||||||
|
this.setState({ includePopular });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,27 +59,57 @@ class DiscoverMovieTableOptions extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
includeRecommendations
|
includeRecommendations,
|
||||||
|
includeTrending,
|
||||||
|
includePopular
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<>
|
||||||
<FormLabel>{translate('IncludeRadarrRecommendations')}</FormLabel>
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludeRadarrRecommendations')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="includeRecommendations"
|
name="includeRecommendations"
|
||||||
value={includeRecommendations}
|
value={includeRecommendations}
|
||||||
helpText={translate('IncludeRecommendationsHelpText')}
|
helpText={translate('IncludeRecommendationsHelpText')}
|
||||||
onChange={this.onChangeOption}
|
onChange={this.onChangeOption}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludeTrending')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includeTrending"
|
||||||
|
value={includeTrending}
|
||||||
|
helpText={translate('IncludeTrendingMoviesHelpText')}
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludePopular')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="includePopular"
|
||||||
|
value={includePopular}
|
||||||
|
helpText={translate('IncludePopularMoviesHelpText')}
|
||||||
|
onChange={this.onChangeOption}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DiscoverMovieTableOptions.propTypes = {
|
DiscoverMovieTableOptions.propTypes = {
|
||||||
includeRecommendations: PropTypes.bool.isRequired,
|
includeRecommendations: PropTypes.bool.isRequired,
|
||||||
|
includeTrending: PropTypes.bool.isRequired,
|
||||||
|
includePopular: PropTypes.bool.isRequired,
|
||||||
onChangeOption: PropTypes.func.isRequired
|
onChangeOption: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { getMovieStatusDetails } from 'Movie/MovieStatus';
|
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||||
import styles from './ListMovieStatusCell.css';
|
import styles from './ListMovieStatusCell.css';
|
||||||
|
|
||||||
function ListMovieStatusCell(props) {
|
function ListMovieStatusCell(props) {
|
||||||
|
|
|
@ -17,6 +17,7 @@ export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
||||||
export const LANGUAGE_SELECT = 'languageSelect';
|
export const LANGUAGE_SELECT = 'languageSelect';
|
||||||
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
|
||||||
export const SELECT = 'select';
|
export const SELECT = 'select';
|
||||||
|
export const MOVIE_TAG = 'movieTag';
|
||||||
export const DYNAMIC_SELECT = 'dynamicSelect';
|
export const DYNAMIC_SELECT = 'dynamicSelect';
|
||||||
export const TAG = 'tag';
|
export const TAG = 'tag';
|
||||||
export const TEXT = 'text';
|
export const TEXT = 'text';
|
||||||
|
@ -45,6 +46,7 @@ export const all = [
|
||||||
INDEXER_FLAGS_SELECT,
|
INDEXER_FLAGS_SELECT,
|
||||||
LANGUAGE_SELECT,
|
LANGUAGE_SELECT,
|
||||||
SELECT,
|
SELECT,
|
||||||
|
MOVIE_TAG,
|
||||||
DYNAMIC_SELECT,
|
DYNAMIC_SELECT,
|
||||||
TAG,
|
TAG,
|
||||||
TEXT,
|
TEXT,
|
||||||
|
|
|
@ -104,7 +104,7 @@ function InteractiveImportRow(props: InteractiveImportRowProps) {
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
if (allowMovieChange && movie && quality && languages) {
|
if (allowMovieChange && movie && quality && languages && size > 0) {
|
||||||
onSelectedChange({
|
onSelectedChange({
|
||||||
id,
|
id,
|
||||||
hasMovieFileId: !!movieFileId,
|
hasMovieFileId: !!movieFileId,
|
||||||
|
|
|
@ -8,18 +8,29 @@ import translate from 'Utilities/String/translate';
|
||||||
import styles from './MovieReleaseDates.css';
|
import styles from './MovieReleaseDates.css';
|
||||||
|
|
||||||
interface MovieReleaseDatesProps {
|
interface MovieReleaseDatesProps {
|
||||||
inCinemas: string;
|
inCinemas?: string;
|
||||||
physicalRelease: string;
|
digitalRelease?: string;
|
||||||
digitalRelease: string;
|
physicalRelease?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
||||||
const { inCinemas, physicalRelease, digitalRelease } = props;
|
const { inCinemas, digitalRelease, physicalRelease } = props;
|
||||||
|
|
||||||
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
||||||
createUISettingsSelector()
|
createUISettingsSelector()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (!inCinemas && !physicalRelease && !digitalRelease) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.dateIcon}>
|
||||||
|
<Icon name={icons.MISSING} />
|
||||||
|
</div>
|
||||||
|
{translate('NoMovieReleaseDatesAvailable')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{inCinemas ? (
|
{inCinemas ? (
|
||||||
|
|
|
@ -15,7 +15,7 @@ function MovieHistoryModal(props) {
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
size={sizes.EXTRA_LARGE}
|
size={sizes.EXTRA_EXTRA_LARGE}
|
||||||
>
|
>
|
||||||
<MovieHistoryModalContentConnector
|
<MovieHistoryModalContentConnector
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
|
|
|
@ -4,13 +4,13 @@ import HistoryDetailsModal from 'Activity/History/Details/HistoryDetailsModal';
|
||||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
import MovieLanguage from 'Movie/MovieLanguage';
|
import MovieLanguage from 'Movie/MovieLanguage';
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './MovieHistoryRow.css';
|
import styles from './MovieHistoryRow.css';
|
||||||
|
@ -109,9 +109,9 @@ class MovieHistoryRow extends Component {
|
||||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<TableRowCell>
|
||||||
date={date}
|
{formatDateTime(date, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||||
/>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.actions}>
|
<TableRowCell className={styles.actions}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
@ -4,7 +4,7 @@ import Icon from 'Components/Icon';
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import { getMovieStatusDetails } from 'Movie/MovieStatus';
|
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||||
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './MovieStatusCell.css';
|
import styles from './MovieStatusCell.css';
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
.movieSearchCell {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 70px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'movieSearchCell': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
|
@ -0,0 +1,81 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import MovieInteractiveSearchModalConnector from './Search/MovieInteractiveSearchModalConnector';
|
||||||
|
import styles from './MovieSearchCell.css';
|
||||||
|
|
||||||
|
class MovieSearchCell extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isInteractiveSearchModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onManualSearchPress = () => {
|
||||||
|
this.setState({ isInteractiveSearchModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onInteractiveSearchModalClose = () => {
|
||||||
|
this.setState({ isInteractiveSearchModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
movieId,
|
||||||
|
movieTitle,
|
||||||
|
isSearching,
|
||||||
|
onSearchPress,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell className={styles.movieSearchCell}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={icons.SEARCH}
|
||||||
|
isSpinning={isSearching}
|
||||||
|
onPress={onSearchPress}
|
||||||
|
title={translate('AutomaticSearch')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
onPress={this.onManualSearchPress}
|
||||||
|
title={translate('InteractiveSearch')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MovieInteractiveSearchModalConnector
|
||||||
|
isOpen={this.state.isInteractiveSearchModalOpen}
|
||||||
|
movieId={movieId}
|
||||||
|
movieTitle={movieTitle}
|
||||||
|
onModalClose={this.onInteractiveSearchModalClose}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieSearchCell.propTypes = {
|
||||||
|
movieId: PropTypes.number.isRequired,
|
||||||
|
movieTitle: PropTypes.string.isRequired,
|
||||||
|
isSearching: PropTypes.bool.isRequired,
|
||||||
|
onSearchPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieSearchCell;
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import MovieSearchCell from 'Movie/MovieSearchCell';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
|
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||||
|
import { isCommandExecuting } from 'Utilities/Command';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { movieId }) => movieId,
|
||||||
|
createMovieSelector(),
|
||||||
|
createCommandsSelector(),
|
||||||
|
(movieId, movie, commands) => {
|
||||||
|
const isSearching = commands.some((command) => {
|
||||||
|
const movieSearch = command.name === commandNames.MOVIE_SEARCH;
|
||||||
|
|
||||||
|
if (!movieSearch) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
isCommandExecuting(command) &&
|
||||||
|
command.body.movieIds.indexOf(movieId) > -1
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
movieMonitored: movie.monitored,
|
||||||
|
isSearching
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onSearchPress(name, path) {
|
||||||
|
dispatch(executeCommand({
|
||||||
|
name: commandNames.MOVIE_SEARCH,
|
||||||
|
movieIds: [props.movieId]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, createMapDispatchToProps)(MovieSearchCell);
|
|
@ -0,0 +1,4 @@
|
||||||
|
.center {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'input': string;
|
'center': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
|
@ -1,33 +1,115 @@
|
||||||
import { icons } from 'Helpers/Props';
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import QueueDetails from 'Activity/Queue/QueueDetails';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import ProgressBar from 'Components/ProgressBar';
|
||||||
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import MovieQuality from './MovieQuality';
|
||||||
|
import styles from './MovieStatus.css';
|
||||||
|
|
||||||
export function getMovieStatusDetails(status) {
|
function MovieStatus(props) {
|
||||||
|
const {
|
||||||
|
isAvailable,
|
||||||
|
monitored,
|
||||||
|
grabbed,
|
||||||
|
queueItem,
|
||||||
|
movieFile
|
||||||
|
} = props;
|
||||||
|
|
||||||
let statusDetails = {
|
const hasMovieFile = !!movieFile;
|
||||||
icon: icons.ANNOUNCED,
|
const isQueued = !!queueItem;
|
||||||
title: translate('Announced'),
|
|
||||||
message: translate('AnnouncedMsg')
|
|
||||||
};
|
|
||||||
|
|
||||||
if (status === 'deleted') {
|
if (isQueued) {
|
||||||
statusDetails = {
|
const {
|
||||||
icon: icons.MOVIE_DELETED,
|
sizeleft,
|
||||||
title: translate('Deleted'),
|
size
|
||||||
message: translate('DeletedMsg')
|
} = queueItem;
|
||||||
};
|
|
||||||
} else if (status === 'inCinemas') {
|
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||||
statusDetails = {
|
|
||||||
icon: icons.IN_CINEMAS,
|
return (
|
||||||
title: translate('InCinemas'),
|
<div className={styles.center}>
|
||||||
message: translate('InCinemasMsg')
|
<QueueDetails
|
||||||
};
|
{...queueItem}
|
||||||
} else if (status === 'released') {
|
progressBar={
|
||||||
statusDetails = {
|
<ProgressBar
|
||||||
icon: icons.MOVIE_FILE,
|
progress={progress}
|
||||||
title: translate('Released'),
|
kind={kinds.PURPLE}
|
||||||
message: translate('ReleasedMsg')
|
size={sizes.MEDIUM}
|
||||||
};
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return statusDetails;
|
if (grabbed) {
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.DOWNLOADING}
|
||||||
|
title={translate('MovieIsDownloading')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMovieFile) {
|
||||||
|
const quality = movieFile.quality;
|
||||||
|
const isCutoffNotMet = movieFile.qualityCutoffNotMet;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<MovieQuality
|
||||||
|
quality={quality}
|
||||||
|
size={movieFile.size}
|
||||||
|
isCutoffNotMet={isCutoffNotMet}
|
||||||
|
title={translate('MovieDownloaded')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!monitored) {
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.UNMONITORED}
|
||||||
|
kind={kinds.DISABLED}
|
||||||
|
title={translate('MovieIsNotMonitored')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAvailable) {
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.MISSING}
|
||||||
|
title={translate('MovieMissingFromDisk')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.center}>
|
||||||
|
<Icon
|
||||||
|
name={icons.NOT_AIRED}
|
||||||
|
title={translate('MovieIsNotAvailable')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MovieStatus.propTypes = {
|
||||||
|
isAvailable: PropTypes.bool.isRequired,
|
||||||
|
monitored: PropTypes.bool.isRequired,
|
||||||
|
grabbed: PropTypes.bool,
|
||||||
|
queueItem: PropTypes.object,
|
||||||
|
movieFile: PropTypes.object
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MovieStatus;
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import MovieStatus from 'Movie/MovieStatus';
|
||||||
|
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
|
||||||
|
import { createMovieByEntitySelector } from 'Store/Selectors/createMovieSelector';
|
||||||
|
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createMovieByEntitySelector(),
|
||||||
|
createQueueItemSelector(),
|
||||||
|
createMovieFileSelector(),
|
||||||
|
(movie, queueItem, movieFile) => {
|
||||||
|
const result = _.pick(movie, [
|
||||||
|
'isAvailable',
|
||||||
|
'monitored',
|
||||||
|
'grabbed'
|
||||||
|
]);
|
||||||
|
|
||||||
|
result.queueItem = queueItem;
|
||||||
|
result.movieFile = movieFile;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MovieStatusConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<MovieStatus
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieStatusConnector.propTypes = {
|
||||||
|
movieId: PropTypes.number.isRequired,
|
||||||
|
movieFileId: PropTypes.number.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, null)(MovieStatusConnector);
|
|
@ -8,6 +8,7 @@ function MovieInteractiveSearchModal(props) {
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
movieId,
|
movieId,
|
||||||
|
movieTitle,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
@ -20,6 +21,7 @@ function MovieInteractiveSearchModal(props) {
|
||||||
>
|
>
|
||||||
<MovieInteractiveSearchModalContent
|
<MovieInteractiveSearchModalContent
|
||||||
movieId={movieId}
|
movieId={movieId}
|
||||||
|
movieTitle={movieTitle}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -29,6 +31,7 @@ function MovieInteractiveSearchModal(props) {
|
||||||
MovieInteractiveSearchModal.propTypes = {
|
MovieInteractiveSearchModal.propTypes = {
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
movieId: PropTypes.number.isRequired,
|
movieId: PropTypes.number.isRequired,
|
||||||
|
movieTitle: PropTypes.string,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,13 +12,17 @@ import translate from 'Utilities/String/translate';
|
||||||
function MovieInteractiveSearchModalContent(props) {
|
function MovieInteractiveSearchModalContent(props) {
|
||||||
const {
|
const {
|
||||||
movieId,
|
movieId,
|
||||||
|
movieTitle,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{translate('InteractiveSearchModalHeader')}
|
{movieTitle === undefined ?
|
||||||
|
translate('InteractiveSearchModalHeader') :
|
||||||
|
translate('InteractiveSearchModalHeaderTitle', { title: movieTitle })
|
||||||
|
}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||||
|
@ -38,6 +42,7 @@ function MovieInteractiveSearchModalContent(props) {
|
||||||
|
|
||||||
MovieInteractiveSearchModalContent.propTypes = {
|
MovieInteractiveSearchModalContent.propTypes = {
|
||||||
movieId: PropTypes.number.isRequired,
|
movieId: PropTypes.number.isRequired,
|
||||||
|
movieTitle: PropTypes.string,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
export default function getMovieStatusDetails(status) {
|
||||||
|
let statusDetails = {
|
||||||
|
icon: icons.ANNOUNCED,
|
||||||
|
title: translate('Announced'),
|
||||||
|
message: translate('AnnouncedMsg')
|
||||||
|
};
|
||||||
|
|
||||||
|
if (status === 'deleted') {
|
||||||
|
statusDetails = {
|
||||||
|
icon: icons.MOVIE_DELETED,
|
||||||
|
title: translate('Deleted'),
|
||||||
|
message: translate('DeletedMsg')
|
||||||
|
};
|
||||||
|
} else if (status === 'inCinemas') {
|
||||||
|
statusDetails = {
|
||||||
|
icon: icons.IN_CINEMAS,
|
||||||
|
title: translate('InCinemas'),
|
||||||
|
message: translate('InCinemasMsg')
|
||||||
|
};
|
||||||
|
} else if (status === 'released') {
|
||||||
|
statusDetails = {
|
||||||
|
icon: icons.MOVIE_FILE,
|
||||||
|
title: translate('Released'),
|
||||||
|
message: translate('ReleasedMsg')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusDetails;
|
||||||
|
}
|
|
@ -1,9 +1,13 @@
|
||||||
export const CALENDAR = 'calendar';
|
export const CALENDAR = 'calendar';
|
||||||
export const MOVIES = 'movies';
|
export const MOVIES = 'movies';
|
||||||
export const INTERACTIVE_IMPORT = 'interactiveImport.movies';
|
export const INTERACTIVE_IMPORT = 'interactiveImport.movies';
|
||||||
|
export const WANTED_CUTOFF_UNMET = 'wanted.cutoffUnmet';
|
||||||
|
export const WANTED_MISSING = 'wanted.missing';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
CALENDAR,
|
CALENDAR,
|
||||||
MOVIES,
|
MOVIES,
|
||||||
INTERACTIVE_IMPORT
|
INTERACTIVE_IMPORT,
|
||||||
|
WANTED_CUTOFF_UNMET,
|
||||||
|
WANTED_MISSING
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@ function createMapStateToProps() {
|
||||||
createMovieFileSelector(),
|
createMovieFileSelector(),
|
||||||
(movieFile) => {
|
(movieFile) => {
|
||||||
return {
|
return {
|
||||||
language: movieFile ? movieFile.language : undefined
|
languages: movieFile ? movieFile.languages : undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,6 +17,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.name {
|
.name {
|
||||||
|
@add-mixin truncate;
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: lighter;
|
font-weight: lighter;
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
|
|
|
@ -72,15 +72,15 @@ const fileNameTokens = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const movieTokens = [
|
const movieTokens = [
|
||||||
{ token: '{Movie Title}', example: 'Movie\'s Title' },
|
{ token: '{Movie Title}', example: 'Movie\'s Title', footNote: 1 },
|
||||||
{ token: '{Movie Title:DE}', example: 'Titel des Films' },
|
{ token: '{Movie Title:DE}', example: 'Titel des Films', footNote: 1 },
|
||||||
{ token: '{Movie CleanTitle}', example: 'Movies Title' },
|
{ token: '{Movie CleanTitle}', example: 'Movies Title', footNote: 1 },
|
||||||
{ token: '{Movie TitleThe}', example: 'Movie\'s Title, The' },
|
{ token: '{Movie TitleThe}', example: 'Movie\'s Title, The', footNote: 1 },
|
||||||
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας' },
|
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNote: 1 },
|
||||||
{ token: '{Movie CleanOriginalTitle}', example: 'Τίτλος ταινίας' },
|
{ token: '{Movie CleanOriginalTitle}', example: 'Τίτλος ταινίας', footNote: 1 },
|
||||||
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
|
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
|
||||||
{ token: '{Movie TitleFirstCharacter:DE}', example: 'T' },
|
{ token: '{Movie TitleFirstCharacter:DE}', example: 'T' },
|
||||||
{ token: '{Movie Collection}', example: 'The Movie Collection' },
|
{ token: '{Movie Collection}', example: 'The Movie Collection', footNote: 1 },
|
||||||
{ token: '{Movie Certification}', example: 'R' },
|
{ token: '{Movie Certification}', example: 'R' },
|
||||||
{ token: '{Release Year}', example: '2009' }
|
{ token: '{Release Year}', example: '2009' }
|
||||||
];
|
];
|
||||||
|
@ -112,15 +112,16 @@ const mediaInfoTokens = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const releaseGroupTokens = [
|
const releaseGroupTokens = [
|
||||||
{ token: '{Release Group}', example: 'Rls Grp' }
|
{ token: '{Release Group}', example: 'Rls Grp', footNote: 1 }
|
||||||
];
|
];
|
||||||
|
|
||||||
const editionTokens = [
|
const editionTokens = [
|
||||||
{ token: '{Edition Tags}', example: 'IMAX' }
|
{ token: '{Edition Tags}', example: 'IMAX', footNote: 1 }
|
||||||
];
|
];
|
||||||
|
|
||||||
const customFormatTokens = [
|
const customFormatTokens = [
|
||||||
{ token: '{Custom Formats}', example: 'Surround Sound x264' }
|
{ token: '{Custom Formats}', example: 'Surround Sound x264' },
|
||||||
|
{ token: '{Custom Format:FormatName}', example: 'AMZN' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const originalTokens = [
|
const originalTokens = [
|
||||||
|
@ -267,7 +268,7 @@ class NamingModal extends Component {
|
||||||
<FieldSet legend={translate('Movie')}>
|
<FieldSet legend={translate('Movie')}>
|
||||||
<div className={styles.groups}>
|
<div className={styles.groups}>
|
||||||
{
|
{
|
||||||
movieTokens.map(({ token, example }) => {
|
movieTokens.map(({ token, example, footNote }) => {
|
||||||
return (
|
return (
|
||||||
<NamingOption
|
<NamingOption
|
||||||
key={token}
|
key={token}
|
||||||
|
@ -275,6 +276,7 @@ class NamingModal extends Component {
|
||||||
value={value}
|
value={value}
|
||||||
token={token}
|
token={token}
|
||||||
example={example}
|
example={example}
|
||||||
|
footNote={footNote}
|
||||||
tokenSeparator={tokenSeparator}
|
tokenSeparator={tokenSeparator}
|
||||||
tokenCase={tokenCase}
|
tokenCase={tokenCase}
|
||||||
onPress={this.onOptionPress}
|
onPress={this.onOptionPress}
|
||||||
|
@ -284,6 +286,11 @@ class NamingModal extends Component {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footNote}>
|
||||||
|
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||||
|
<InlineMarkdown data={translate('MovieFootNote')} />
|
||||||
|
</div>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('MovieID')}>
|
<FieldSet legend={translate('MovieID')}>
|
||||||
|
@ -364,7 +371,7 @@ class NamingModal extends Component {
|
||||||
<FieldSet legend={translate('ReleaseGroup')}>
|
<FieldSet legend={translate('ReleaseGroup')}>
|
||||||
<div className={styles.groups}>
|
<div className={styles.groups}>
|
||||||
{
|
{
|
||||||
releaseGroupTokens.map(({ token, example }) => {
|
releaseGroupTokens.map(({ token, example, footNote }) => {
|
||||||
return (
|
return (
|
||||||
<NamingOption
|
<NamingOption
|
||||||
key={token}
|
key={token}
|
||||||
|
@ -372,6 +379,7 @@ class NamingModal extends Component {
|
||||||
value={value}
|
value={value}
|
||||||
token={token}
|
token={token}
|
||||||
example={example}
|
example={example}
|
||||||
|
footNote={footNote}
|
||||||
tokenSeparator={tokenSeparator}
|
tokenSeparator={tokenSeparator}
|
||||||
tokenCase={tokenCase}
|
tokenCase={tokenCase}
|
||||||
onPress={this.onOptionPress}
|
onPress={this.onOptionPress}
|
||||||
|
@ -381,12 +389,17 @@ class NamingModal extends Component {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footNote}>
|
||||||
|
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||||
|
<InlineMarkdown data={translate('ReleaseGroupFootNote')} />
|
||||||
|
</div>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('Edition')}>
|
<FieldSet legend={translate('Edition')}>
|
||||||
<div className={styles.groups}>
|
<div className={styles.groups}>
|
||||||
{
|
{
|
||||||
editionTokens.map(({ token, example }) => {
|
editionTokens.map(({ token, example, footNote }) => {
|
||||||
return (
|
return (
|
||||||
<NamingOption
|
<NamingOption
|
||||||
key={token}
|
key={token}
|
||||||
|
@ -394,6 +407,7 @@ class NamingModal extends Component {
|
||||||
value={value}
|
value={value}
|
||||||
token={token}
|
token={token}
|
||||||
example={example}
|
example={example}
|
||||||
|
footNote={footNote}
|
||||||
tokenSeparator={tokenSeparator}
|
tokenSeparator={tokenSeparator}
|
||||||
tokenCase={tokenCase}
|
tokenCase={tokenCase}
|
||||||
onPress={this.onOptionPress}
|
onPress={this.onOptionPress}
|
||||||
|
@ -403,6 +417,11 @@ class NamingModal extends Component {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footNote}>
|
||||||
|
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||||
|
<InlineMarkdown data={translate('EditionFootNote')} />
|
||||||
|
</div>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend={translate('CustomFormats')}>
|
<FieldSet legend={translate('CustomFormats')}>
|
||||||
|
|
|
@ -12,7 +12,7 @@ export default function TagInUse(props) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count > 1 && labelPlural ) {
|
if (count > 1 && labelPlural) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{count} {labelPlural.toLowerCase()}
|
{count} {labelPlural.toLowerCase()}
|
||||||
|
|
|
@ -1,29 +1,29 @@
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
import updateEpisodes from 'Utilities/Episode/updateEpisodes';
|
import updateMovies from 'Utilities/Movie/updateMovies';
|
||||||
import getSectionState from 'Utilities/State/getSectionState';
|
import getSectionState from 'Utilities/State/getSectionState';
|
||||||
|
|
||||||
function createBatchToggleEpisodeMonitoredHandler(section, fetchHandler) {
|
function createBatchToggleMovieMonitoredHandler(section, fetchHandler) {
|
||||||
return function(getState, payload, dispatch) {
|
return function(getState, payload, dispatch) {
|
||||||
const {
|
const {
|
||||||
episodeIds,
|
movieIds,
|
||||||
monitored
|
monitored
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
const state = getSectionState(getState(), section, true);
|
const state = getSectionState(getState(), section, true);
|
||||||
|
|
||||||
dispatch(updateEpisodes(section, state.items, episodeIds, {
|
dispatch(updateMovies(section, state.items, movieIds, {
|
||||||
isSaving: true
|
isSaving: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
const promise = createAjaxRequest({
|
||||||
url: '/episode/monitor',
|
url: '/movie/editor',
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
data: JSON.stringify({ episodeIds, monitored }),
|
data: JSON.stringify({ movieIds, monitored }),
|
||||||
dataType: 'json'
|
dataType: 'json'
|
||||||
}).request;
|
}).request;
|
||||||
|
|
||||||
promise.done(() => {
|
promise.done(() => {
|
||||||
dispatch(updateEpisodes(section, state.items, episodeIds, {
|
dispatch(updateMovies(section, state.items, movieIds, {
|
||||||
isSaving: false,
|
isSaving: false,
|
||||||
monitored
|
monitored
|
||||||
}));
|
}));
|
||||||
|
@ -32,11 +32,11 @@ function createBatchToggleEpisodeMonitoredHandler(section, fetchHandler) {
|
||||||
});
|
});
|
||||||
|
|
||||||
promise.fail(() => {
|
promise.fail(() => {
|
||||||
dispatch(updateEpisodes(section, state.items, episodeIds, {
|
dispatch(updateMovies(section, state.items, movieIds, {
|
||||||
isSaving: false
|
isSaving: false
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createBatchToggleEpisodeMonitoredHandler;
|
export default createBatchToggleMovieMonitoredHandler;
|
|
@ -1,8 +1,11 @@
|
||||||
|
import $ from 'jquery';
|
||||||
|
import _ from 'lodash';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
import getProviderState from 'Utilities/State/getProviderState';
|
import getProviderState from 'Utilities/State/getProviderState';
|
||||||
import { set } from '../baseActions';
|
import { set } from '../baseActions';
|
||||||
|
|
||||||
const abortCurrentRequests = {};
|
const abortCurrentRequests = {};
|
||||||
|
let lastTestData = null;
|
||||||
|
|
||||||
export function createCancelTestProviderHandler(section) {
|
export function createCancelTestProviderHandler(section) {
|
||||||
return function(getState, payload, dispatch) {
|
return function(getState, payload, dispatch) {
|
||||||
|
@ -17,10 +20,25 @@ function createTestProviderHandler(section, url) {
|
||||||
return function(getState, payload, dispatch) {
|
return function(getState, payload, dispatch) {
|
||||||
dispatch(set({ section, isTesting: true }));
|
dispatch(set({ section, isTesting: true }));
|
||||||
|
|
||||||
const testData = getProviderState(payload, getState, section);
|
const {
|
||||||
|
queryParams = {},
|
||||||
|
...otherPayload
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
const testData = getProviderState({ ...otherPayload }, getState, section);
|
||||||
|
const params = { ...queryParams };
|
||||||
|
|
||||||
|
// If the user is re-testing the same provider without changes
|
||||||
|
// force it to be tested.
|
||||||
|
|
||||||
|
if (_.isEqual(testData, lastTestData)) {
|
||||||
|
params.forceTest = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTestData = testData;
|
||||||
|
|
||||||
const ajaxOptions = {
|
const ajaxOptions = {
|
||||||
url: `${url}/test`,
|
url: `${url}/test?${$.param(params, true)}`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
|
@ -32,6 +50,8 @@ function createTestProviderHandler(section, url) {
|
||||||
abortCurrentRequests[section] = abortRequest;
|
abortCurrentRequests[section] = abortRequest;
|
||||||
|
|
||||||
request.done((data) => {
|
request.done((data) => {
|
||||||
|
lastTestData = null;
|
||||||
|
|
||||||
dispatch(set({
|
dispatch(set({
|
||||||
section,
|
section,
|
||||||
isTesting: false,
|
isTesting: false,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import { batchActions } from 'redux-batched-actions';
|
import { batchActions } from 'redux-batched-actions';
|
||||||
import { sortDirections } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||||
|
@ -79,6 +79,31 @@ export const defaultState = {
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
isModifiable: false
|
isModifiable: false
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
selectedFilterKey: 'all',
|
||||||
|
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'all',
|
||||||
|
label: () => translate('All'),
|
||||||
|
filters: []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
filterBuilderProps: [
|
||||||
|
{
|
||||||
|
name: 'movieIds',
|
||||||
|
label: () => translate('Movie'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.MOVIE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'protocols',
|
||||||
|
label: () => translate('Protocol'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.PROTOCOL
|
||||||
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -86,6 +111,7 @@ export const persistState = [
|
||||||
'blocklist.pageSize',
|
'blocklist.pageSize',
|
||||||
'blocklist.sortKey',
|
'blocklist.sortKey',
|
||||||
'blocklist.sortDirection',
|
'blocklist.sortDirection',
|
||||||
|
'blocklist.selectedFilterKey',
|
||||||
'blocklist.columns'
|
'blocklist.columns'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -99,6 +125,7 @@ export const GOTO_NEXT_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistNextPage';
|
||||||
export const GOTO_LAST_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistLastPage';
|
export const GOTO_LAST_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistLastPage';
|
||||||
export const GOTO_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistPage';
|
export const GOTO_BLOCKLIST_PAGE = 'blocklist/gotoBlocklistPage';
|
||||||
export const SET_BLOCKLIST_SORT = 'blocklist/setBlocklistSort';
|
export const SET_BLOCKLIST_SORT = 'blocklist/setBlocklistSort';
|
||||||
|
export const SET_BLOCKLIST_FILTER = 'blocklist/setBlocklistFilter';
|
||||||
export const SET_BLOCKLIST_TABLE_OPTION = 'blocklist/setBlocklistTableOption';
|
export const SET_BLOCKLIST_TABLE_OPTION = 'blocklist/setBlocklistTableOption';
|
||||||
export const REMOVE_BLOCKLIST_ITEM = 'blocklist/removeBlocklistItem';
|
export const REMOVE_BLOCKLIST_ITEM = 'blocklist/removeBlocklistItem';
|
||||||
export const REMOVE_BLOCKLIST_ITEMS = 'blocklist/removeBlocklistItems';
|
export const REMOVE_BLOCKLIST_ITEMS = 'blocklist/removeBlocklistItems';
|
||||||
|
@ -114,6 +141,7 @@ export const gotoBlocklistNextPage = createThunk(GOTO_NEXT_BLOCKLIST_PAGE);
|
||||||
export const gotoBlocklistLastPage = createThunk(GOTO_LAST_BLOCKLIST_PAGE);
|
export const gotoBlocklistLastPage = createThunk(GOTO_LAST_BLOCKLIST_PAGE);
|
||||||
export const gotoBlocklistPage = createThunk(GOTO_BLOCKLIST_PAGE);
|
export const gotoBlocklistPage = createThunk(GOTO_BLOCKLIST_PAGE);
|
||||||
export const setBlocklistSort = createThunk(SET_BLOCKLIST_SORT);
|
export const setBlocklistSort = createThunk(SET_BLOCKLIST_SORT);
|
||||||
|
export const setBlocklistFilter = createThunk(SET_BLOCKLIST_FILTER);
|
||||||
export const setBlocklistTableOption = createAction(SET_BLOCKLIST_TABLE_OPTION);
|
export const setBlocklistTableOption = createAction(SET_BLOCKLIST_TABLE_OPTION);
|
||||||
export const removeBlocklistItem = createThunk(REMOVE_BLOCKLIST_ITEM);
|
export const removeBlocklistItem = createThunk(REMOVE_BLOCKLIST_ITEM);
|
||||||
export const removeBlocklistItems = createThunk(REMOVE_BLOCKLIST_ITEMS);
|
export const removeBlocklistItems = createThunk(REMOVE_BLOCKLIST_ITEMS);
|
||||||
|
@ -134,7 +162,8 @@ export const actionHandlers = handleThunks({
|
||||||
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_BLOCKLIST_PAGE,
|
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_BLOCKLIST_PAGE,
|
||||||
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_BLOCKLIST_PAGE,
|
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_BLOCKLIST_PAGE,
|
||||||
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_BLOCKLIST_PAGE,
|
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_BLOCKLIST_PAGE,
|
||||||
[serverSideCollectionHandlers.SORT]: SET_BLOCKLIST_SORT
|
[serverSideCollectionHandlers.SORT]: SET_BLOCKLIST_SORT,
|
||||||
|
[serverSideCollectionHandlers.FILTER]: SET_BLOCKLIST_FILTER
|
||||||
}),
|
}),
|
||||||
|
|
||||||
[REMOVE_BLOCKLIST_ITEM]: createRemoveItemHandler(section, '/blocklist'),
|
[REMOVE_BLOCKLIST_ITEM]: createRemoveItemHandler(section, '/blocklist'),
|
||||||
|
|
|
@ -42,7 +42,9 @@ export const defaultState = {
|
||||||
view: 'overview',
|
view: 'overview',
|
||||||
|
|
||||||
options: {
|
options: {
|
||||||
includeRecommendations: true
|
includeRecommendations: true,
|
||||||
|
includeTrending: true,
|
||||||
|
includePopular: true
|
||||||
},
|
},
|
||||||
|
|
||||||
defaults: {
|
defaults: {
|
||||||
|
@ -583,10 +585,14 @@ export const actionHandlers = handleThunks({
|
||||||
...otherPayload
|
...otherPayload
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
const includeRecommendations = getState().discoverMovie.options.includeRecommendations;
|
const {
|
||||||
|
includeRecommendations = false,
|
||||||
|
includeTrending = false,
|
||||||
|
includePopular = false
|
||||||
|
} = getState().discoverMovie.options;
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
const promise = createAjaxRequest({
|
||||||
url: `/importlist/movie?includeRecommendations=${includeRecommendations}`,
|
url: `/importlist/movie?includeRecommendations=${includeRecommendations}&includeTrending=${includeTrending}&includePopular=${includePopular}`,
|
||||||
data: otherPayload,
|
data: otherPayload,
|
||||||
traditional: true
|
traditional: true
|
||||||
}).request;
|
}).request;
|
||||||
|
|
|
@ -28,6 +28,7 @@ import * as rootFolders from './rootFolderActions';
|
||||||
import * as settings from './settingsActions';
|
import * as settings from './settingsActions';
|
||||||
import * as system from './systemActions';
|
import * as system from './systemActions';
|
||||||
import * as tags from './tagActions';
|
import * as tags from './tagActions';
|
||||||
|
import * as wanted from './wantedActions';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
addMovie,
|
addMovie,
|
||||||
|
@ -59,5 +60,6 @@ export default [
|
||||||
movieCredits,
|
movieCredits,
|
||||||
settings,
|
settings,
|
||||||
system,
|
system,
|
||||||
tags
|
tags,
|
||||||
|
wanted
|
||||||
];
|
];
|
||||||
|
|
|
@ -0,0 +1,298 @@
|
||||||
|
import { createAction } from 'redux-actions';
|
||||||
|
import { filterTypes, sortDirections } from 'Helpers/Props';
|
||||||
|
import createBatchToggleMovieMonitoredHandler from 'Store/Actions/Creators/createBatchToggleMovieMonitoredHandler';
|
||||||
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
|
import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers';
|
||||||
|
import createClearReducer from './Creators/Reducers/createClearReducer';
|
||||||
|
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Variables
|
||||||
|
|
||||||
|
export const section = 'wanted';
|
||||||
|
|
||||||
|
//
|
||||||
|
// State
|
||||||
|
|
||||||
|
export const defaultState = {
|
||||||
|
missing: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
pageSize: 20,
|
||||||
|
sortKey: 'movieMetadata.sortTitle',
|
||||||
|
sortDirection: sortDirections.ASCENDING,
|
||||||
|
error: null,
|
||||||
|
items: [],
|
||||||
|
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'movieMetadata.sortTitle',
|
||||||
|
label: () => translate('MovieTitle'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'movieMetadata.year',
|
||||||
|
label: () => translate('Year'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
label: () => translate('Status'),
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
columnLabel: () => translate('Actions'),
|
||||||
|
isVisible: true,
|
||||||
|
isModifiable: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
selectedFilterKey: 'monitored',
|
||||||
|
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'monitored',
|
||||||
|
label: () => translate('Monitored'),
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'monitored',
|
||||||
|
value: true,
|
||||||
|
type: filterTypes.EQUAL
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'unmonitored',
|
||||||
|
label: () => translate('Unmonitored'),
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'monitored',
|
||||||
|
value: false,
|
||||||
|
type: filterTypes.EQUAL
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
cutoffUnmet: {
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
pageSize: 20,
|
||||||
|
sortKey: 'movieMetadata.sortTitle',
|
||||||
|
sortDirection: sortDirections.ASCENDING,
|
||||||
|
items: [],
|
||||||
|
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'movieMetadata.sortTitle',
|
||||||
|
label: () => translate('MovieTitle'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'movieMetadata.year',
|
||||||
|
label: () => translate('Year'),
|
||||||
|
isSortable: true,
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'languages',
|
||||||
|
label: () => translate('Languages'),
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
label: () => translate('Status'),
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
columnLabel: () => translate('Actions'),
|
||||||
|
isVisible: true,
|
||||||
|
isModifiable: false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
selectedFilterKey: 'monitored',
|
||||||
|
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'monitored',
|
||||||
|
label: () => translate('Monitored'),
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'monitored',
|
||||||
|
value: true,
|
||||||
|
type: filterTypes.EQUAL
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'unmonitored',
|
||||||
|
label: () => translate('Unmonitored'),
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
key: 'monitored',
|
||||||
|
value: false,
|
||||||
|
type: filterTypes.EQUAL
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const persistState = [
|
||||||
|
'wanted.missing.pageSize',
|
||||||
|
'wanted.missing.sortKey',
|
||||||
|
'wanted.missing.sortDirection',
|
||||||
|
'wanted.missing.selectedFilterKey',
|
||||||
|
'wanted.missing.columns',
|
||||||
|
'wanted.cutoffUnmet.pageSize',
|
||||||
|
'wanted.cutoffUnmet.sortKey',
|
||||||
|
'wanted.cutoffUnmet.sortDirection',
|
||||||
|
'wanted.cutoffUnmet.selectedFilterKey',
|
||||||
|
'wanted.cutoffUnmet.columns'
|
||||||
|
];
|
||||||
|
|
||||||
|
//
|
||||||
|
// Actions Types
|
||||||
|
|
||||||
|
export const FETCH_MISSING = 'wanted/missing/fetchMissing';
|
||||||
|
export const GOTO_FIRST_MISSING_PAGE = 'wanted/missing/gotoMissingFirstPage';
|
||||||
|
export const GOTO_PREVIOUS_MISSING_PAGE = 'wanted/missing/gotoMissingPreviousPage';
|
||||||
|
export const GOTO_NEXT_MISSING_PAGE = 'wanted/missing/gotoMissingNextPage';
|
||||||
|
export const GOTO_LAST_MISSING_PAGE = 'wanted/missing/gotoMissingLastPage';
|
||||||
|
export const GOTO_MISSING_PAGE = 'wanted/missing/gotoMissingPage';
|
||||||
|
export const SET_MISSING_SORT = 'wanted/missing/setMissingSort';
|
||||||
|
export const SET_MISSING_FILTER = 'wanted/missing/setMissingFilter';
|
||||||
|
export const SET_MISSING_TABLE_OPTION = 'wanted/missing/setMissingTableOption';
|
||||||
|
export const CLEAR_MISSING = 'wanted/missing/clearMissing';
|
||||||
|
|
||||||
|
export const BATCH_TOGGLE_MISSING_MOVIES = 'wanted/missing/batchToggleMissingMovies';
|
||||||
|
|
||||||
|
export const FETCH_CUTOFF_UNMET = 'wanted/cutoffUnmet/fetchCutoffUnmet';
|
||||||
|
export const GOTO_FIRST_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetFirstPage';
|
||||||
|
export const GOTO_PREVIOUS_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetPreviousPage';
|
||||||
|
export const GOTO_NEXT_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetNextPage';
|
||||||
|
export const GOTO_LAST_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetFastPage';
|
||||||
|
export const GOTO_CUTOFF_UNMET_PAGE = 'wanted/cutoffUnmet/gotoCutoffUnmetPage';
|
||||||
|
export const SET_CUTOFF_UNMET_SORT = 'wanted/cutoffUnmet/setCutoffUnmetSort';
|
||||||
|
export const SET_CUTOFF_UNMET_FILTER = 'wanted/cutoffUnmet/setCutoffUnmetFilter';
|
||||||
|
export const SET_CUTOFF_UNMET_TABLE_OPTION = 'wanted/cutoffUnmet/setCutoffUnmetTableOption';
|
||||||
|
export const CLEAR_CUTOFF_UNMET = 'wanted/cutoffUnmet/clearCutoffUnmet';
|
||||||
|
|
||||||
|
export const BATCH_TOGGLE_CUTOFF_UNMET_MOVIES = 'wanted/cutoffUnmet/batchToggleCutoffUnmetMovies';
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Creators
|
||||||
|
|
||||||
|
export const fetchMissing = createThunk(FETCH_MISSING);
|
||||||
|
export const gotoMissingFirstPage = createThunk(GOTO_FIRST_MISSING_PAGE);
|
||||||
|
export const gotoMissingPreviousPage = createThunk(GOTO_PREVIOUS_MISSING_PAGE);
|
||||||
|
export const gotoMissingNextPage = createThunk(GOTO_NEXT_MISSING_PAGE);
|
||||||
|
export const gotoMissingLastPage = createThunk(GOTO_LAST_MISSING_PAGE);
|
||||||
|
export const gotoMissingPage = createThunk(GOTO_MISSING_PAGE);
|
||||||
|
export const setMissingSort = createThunk(SET_MISSING_SORT);
|
||||||
|
export const setMissingFilter = createThunk(SET_MISSING_FILTER);
|
||||||
|
export const setMissingTableOption = createAction(SET_MISSING_TABLE_OPTION);
|
||||||
|
export const clearMissing = createAction(CLEAR_MISSING);
|
||||||
|
|
||||||
|
export const batchToggleMissingMovies = createThunk(BATCH_TOGGLE_MISSING_MOVIES);
|
||||||
|
|
||||||
|
export const fetchCutoffUnmet = createThunk(FETCH_CUTOFF_UNMET);
|
||||||
|
export const gotoCutoffUnmetFirstPage = createThunk(GOTO_FIRST_CUTOFF_UNMET_PAGE);
|
||||||
|
export const gotoCutoffUnmetPreviousPage = createThunk(GOTO_PREVIOUS_CUTOFF_UNMET_PAGE);
|
||||||
|
export const gotoCutoffUnmetNextPage = createThunk(GOTO_NEXT_CUTOFF_UNMET_PAGE);
|
||||||
|
export const gotoCutoffUnmetLastPage = createThunk(GOTO_LAST_CUTOFF_UNMET_PAGE);
|
||||||
|
export const gotoCutoffUnmetPage = createThunk(GOTO_CUTOFF_UNMET_PAGE);
|
||||||
|
export const setCutoffUnmetSort = createThunk(SET_CUTOFF_UNMET_SORT);
|
||||||
|
export const setCutoffUnmetFilter = createThunk(SET_CUTOFF_UNMET_FILTER);
|
||||||
|
export const setCutoffUnmetTableOption = createAction(SET_CUTOFF_UNMET_TABLE_OPTION);
|
||||||
|
export const clearCutoffUnmet = createAction(CLEAR_CUTOFF_UNMET);
|
||||||
|
|
||||||
|
export const batchToggleCutoffUnmetMovies = createThunk(BATCH_TOGGLE_CUTOFF_UNMET_MOVIES);
|
||||||
|
|
||||||
|
//
|
||||||
|
// Action Handlers
|
||||||
|
|
||||||
|
export const actionHandlers = handleThunks({
|
||||||
|
|
||||||
|
...createServerSideCollectionHandlers(
|
||||||
|
'wanted.missing',
|
||||||
|
'/wanted/missing',
|
||||||
|
fetchMissing,
|
||||||
|
{
|
||||||
|
[serverSideCollectionHandlers.FETCH]: FETCH_MISSING,
|
||||||
|
[serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_MISSING_PAGE,
|
||||||
|
[serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_MISSING_PAGE,
|
||||||
|
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_MISSING_PAGE,
|
||||||
|
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_MISSING_PAGE,
|
||||||
|
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_MISSING_PAGE,
|
||||||
|
[serverSideCollectionHandlers.SORT]: SET_MISSING_SORT,
|
||||||
|
[serverSideCollectionHandlers.FILTER]: SET_MISSING_FILTER
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
[BATCH_TOGGLE_MISSING_MOVIES]: createBatchToggleMovieMonitoredHandler('wanted.missing', fetchMissing),
|
||||||
|
|
||||||
|
...createServerSideCollectionHandlers(
|
||||||
|
'wanted.cutoffUnmet',
|
||||||
|
'/wanted/cutoff',
|
||||||
|
fetchCutoffUnmet,
|
||||||
|
{
|
||||||
|
[serverSideCollectionHandlers.FETCH]: FETCH_CUTOFF_UNMET,
|
||||||
|
[serverSideCollectionHandlers.FIRST_PAGE]: GOTO_FIRST_CUTOFF_UNMET_PAGE,
|
||||||
|
[serverSideCollectionHandlers.PREVIOUS_PAGE]: GOTO_PREVIOUS_CUTOFF_UNMET_PAGE,
|
||||||
|
[serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_CUTOFF_UNMET_PAGE,
|
||||||
|
[serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_CUTOFF_UNMET_PAGE,
|
||||||
|
[serverSideCollectionHandlers.EXACT_PAGE]: GOTO_CUTOFF_UNMET_PAGE,
|
||||||
|
[serverSideCollectionHandlers.SORT]: SET_CUTOFF_UNMET_SORT,
|
||||||
|
[serverSideCollectionHandlers.FILTER]: SET_CUTOFF_UNMET_FILTER
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
[BATCH_TOGGLE_CUTOFF_UNMET_MOVIES]: createBatchToggleMovieMonitoredHandler('wanted.cutoffUnmet', fetchCutoffUnmet)
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
// Reducers
|
||||||
|
|
||||||
|
export const reducers = createHandleActions({
|
||||||
|
|
||||||
|
[SET_MISSING_TABLE_OPTION]: createSetTableOptionReducer('wanted.missing'),
|
||||||
|
[SET_CUTOFF_UNMET_TABLE_OPTION]: createSetTableOptionReducer('wanted.cutoffUnmet'),
|
||||||
|
|
||||||
|
[CLEAR_MISSING]: createClearReducer(
|
||||||
|
'wanted.missing',
|
||||||
|
{
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
items: [],
|
||||||
|
totalPages: 0,
|
||||||
|
totalRecords: 0
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
[CLEAR_CUTOFF_UNMET]: createClearReducer(
|
||||||
|
'wanted.cutoffUnmet',
|
||||||
|
{
|
||||||
|
isFetching: false,
|
||||||
|
isPopulated: false,
|
||||||
|
error: null,
|
||||||
|
items: [],
|
||||||
|
totalPages: 0,
|
||||||
|
totalRecords: 0
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}, defaultState, section);
|
|
@ -1,4 +1,6 @@
|
||||||
|
import _ from 'lodash';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import movieEntities from 'Movie/movieEntities';
|
||||||
|
|
||||||
export function createMovieSelectorForHook(movieId) {
|
export function createMovieSelectorForHook(movieId) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
@ -11,6 +13,16 @@ export function createMovieSelectorForHook(movieId) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createMovieByEntitySelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { movieId }) => movieId,
|
||||||
|
(state, { movieEntity = movieEntities.MOVIES }) => _.get(state, movieEntity, { items: [] }),
|
||||||
|
(movieId, movies) => {
|
||||||
|
return _.find(movies.items, { id: movieId });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function createMovieSelector() {
|
function createMovieSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { movieId }) => movieId,
|
(state, { movieId }) => movieId,
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as dark from './dark';
|
||||||
import * as light from './light';
|
import * as light from './light';
|
||||||
|
|
||||||
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
const defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
const auto = defaultDark ? { ...dark } : { ...light };
|
const auto = defaultDark ? dark : light;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
auto,
|
auto,
|
||||||
|
|
|
@ -2,7 +2,6 @@ module.exports = {
|
||||||
// Families
|
// Families
|
||||||
defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
defaultFontFamily: 'Roboto, "open sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
||||||
monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;',
|
monoSpaceFontFamily: '"Ubuntu Mono", Menlo, Monaco, Consolas, "Courier New", monospace;',
|
||||||
passwordFamily: 'text-security-disc',
|
|
||||||
|
|
||||||
// Sizes
|
// Sizes
|
||||||
extraSmallFontSize: '11px',
|
extraSmallFontSize: '11px',
|
||||||
|
|
|
@ -6,6 +6,22 @@ import createMultiMoviesSelector from 'Store/Selectors/createMultiMoviesSelector
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './QueuedTaskRowNameCell.css';
|
import styles from './QueuedTaskRowNameCell.css';
|
||||||
|
|
||||||
|
function formatTitles(titles: string[]) {
|
||||||
|
if (!titles) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (titles.length > 11) {
|
||||||
|
return (
|
||||||
|
<span title={titles.join(', ')}>
|
||||||
|
{titles.slice(0, 10).join(', ')}, {titles.length - 10} more
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{titles.join(', ')}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface QueuedTaskRowNameCellProps {
|
export interface QueuedTaskRowNameCellProps {
|
||||||
commandName: string;
|
commandName: string;
|
||||||
body: CommandBody;
|
body: CommandBody;
|
||||||
|
@ -32,7 +48,7 @@ export default function QueuedTaskRowNameCell(
|
||||||
<span className={styles.commandName}>
|
<span className={styles.commandName}>
|
||||||
{commandName}
|
{commandName}
|
||||||
{sortedMovies.length ? (
|
{sortedMovies.length ? (
|
||||||
<span> - {sortedMovies.map((m) => m.title).join(', ')}</span>
|
<span> - {formatTitles(sortedMovies.map((m) => m.title))}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { update } from 'Store/Actions/baseActions';
|
import { update } from 'Store/Actions/baseActions';
|
||||||
|
|
||||||
function updateEpisodes(section, episodes, episodeIds, options) {
|
function updateMovies(section, movies, movieIds, options) {
|
||||||
const data = _.reduce(episodes, (result, item) => {
|
const data = _.reduce(movies, (result, item) => {
|
||||||
if (episodeIds.indexOf(item.id) > -1) {
|
if (movieIds.indexOf(item.id) > -1) {
|
||||||
result.push({
|
result.push({
|
||||||
...item,
|
...item,
|
||||||
...options
|
...options
|
||||||
|
@ -18,4 +18,4 @@ function updateEpisodes(section, episodes, episodeIds, options) {
|
||||||
return update({ section, data });
|
return update({ section, data });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default updateEpisodes;
|
export default updateMovies;
|
|
@ -0,0 +1,301 @@
|
||||||
|
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 ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import getFilterValue from 'Utilities/Filter/getFilterValue';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
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 CutoffUnmetRow from './CutoffUnmetRow';
|
||||||
|
|
||||||
|
function getMonitoredValue(props) {
|
||||||
|
const {
|
||||||
|
filters,
|
||||||
|
selectedFilterKey
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return getFilterValue(filters, selectedFilterKey, 'monitored', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CutoffUnmet extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: false,
|
||||||
|
lastToggled: null,
|
||||||
|
selectedState: {},
|
||||||
|
isConfirmSearchAllCutoffUnmetModalOpen: false,
|
||||||
|
isInteractiveImportModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||||
|
this.setState((state) => {
|
||||||
|
return removeOldSelectedState(state, prevProps.items);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
getSelectedIds = () => {
|
||||||
|
return getSelectedIds(this.state.selectedState);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onFilterMenuItemPress = (filterKey, filterValue) => {
|
||||||
|
this.props.onFilterSelect(filterKey, filterValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectAllChange = ({ value }) => {
|
||||||
|
this.setState(selectAll(this.state.selectedState, value));
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||||
|
this.setState((state) => {
|
||||||
|
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchSelectedPress = () => {
|
||||||
|
const selected = this.getSelectedIds();
|
||||||
|
|
||||||
|
this.props.onSearchSelectedPress(selected);
|
||||||
|
};
|
||||||
|
|
||||||
|
onToggleSelectedPress = () => {
|
||||||
|
const movieIds = this.getSelectedIds();
|
||||||
|
|
||||||
|
this.props.batchToggleCutoffUnmetMovies({
|
||||||
|
movieIds,
|
||||||
|
monitored: !getMonitoredValue(this.props)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchAllCutoffUnmetPress = () => {
|
||||||
|
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchAllCutoffUnmetConfirmed = () => {
|
||||||
|
const {
|
||||||
|
selectedFilterKey,
|
||||||
|
onSearchAllCutoffUnmetPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// TODO: Custom filters will need to check whether there is a monitored
|
||||||
|
// filter once implemented.
|
||||||
|
|
||||||
|
onSearchAllCutoffUnmetPress(selectedFilterKey === 'monitored');
|
||||||
|
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onConfirmSearchAllCutoffUnmetModalClose = () => {
|
||||||
|
this.setState({ isConfirmSearchAllCutoffUnmetModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
columns,
|
||||||
|
totalRecords,
|
||||||
|
isSearchingForCutoffUnmetMovies,
|
||||||
|
isSaving,
|
||||||
|
onFilterSelect,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
selectedState,
|
||||||
|
isConfirmSearchAllCutoffUnmetModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const itemsSelected = !!this.getSelectedIds().length;
|
||||||
|
const isShowingMonitored = getMonitoredValue(this.props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('CutoffUnmet')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('SearchSelected')}
|
||||||
|
iconName={icons.SEARCH}
|
||||||
|
isDisabled={!itemsSelected || isSearchingForCutoffUnmetMovies}
|
||||||
|
onPress={this.onSearchSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
|
||||||
|
iconName={icons.MONITORED}
|
||||||
|
isDisabled={!itemsSelected}
|
||||||
|
isSpinning={isSaving}
|
||||||
|
onPress={this.onToggleSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('SearchAll')}
|
||||||
|
iconName={icons.SEARCH}
|
||||||
|
isDisabled={!items.length}
|
||||||
|
isSpinning={isSearchingForCutoffUnmetMovies}
|
||||||
|
onPress={this.onSearchAllCutoffUnmetPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
{...otherProps}
|
||||||
|
columns={columns}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={[]}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
{
|
||||||
|
isFetching && !isPopulated &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && error &&
|
||||||
|
<Alert kind={kinds.DANGER}>
|
||||||
|
{translate('CutoffUnmetLoadError')}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error && !items.length &&
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
{translate('CutoffUnmetNoItems')}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error && !!items.length &&
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
{...otherProps}
|
||||||
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<CutoffUnmetRow
|
||||||
|
key={item.id}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
onSelectedChange={this.onSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmSearchAllCutoffUnmetModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('SearchForCutoffUnmetMovies')}
|
||||||
|
message={
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{translate('SearchForCutoffUnmetMoviesConfirmationCount', { totalRecords })}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{translate('MassSearchCancelWarning')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
confirmLabel={translate('Search')}
|
||||||
|
onConfirm={this.onSearchAllCutoffUnmetConfirmed}
|
||||||
|
onCancel={this.onConfirmSearchAllCutoffUnmetModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CutoffUnmet.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
totalRecords: PropTypes.number,
|
||||||
|
isSearchingForCutoffUnmetMovies: PropTypes.bool.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
|
onSearchSelectedPress: PropTypes.func.isRequired,
|
||||||
|
batchToggleCutoffUnmetMovies: PropTypes.func.isRequired,
|
||||||
|
onSearchAllCutoffUnmetPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CutoffUnmet;
|
|
@ -0,0 +1,185 @@
|
||||||
|
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 withCurrentPage from 'Components/withCurrentPage';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||||
|
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||||
|
import * as wantedActions from 'Store/Actions/wantedActions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
import CutoffUnmet from './CutoffUnmet';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.wanted.cutoffUnmet,
|
||||||
|
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_MOVIES_SEARCH),
|
||||||
|
(cutoffUnmet, isSearchingForCutoffUnmetMovies) => {
|
||||||
|
return {
|
||||||
|
isSearchingForCutoffUnmetMovies,
|
||||||
|
isSaving: cutoffUnmet.items.filter((m) => m.isSaving).length > 1,
|
||||||
|
...cutoffUnmet
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
...wantedActions,
|
||||||
|
executeCommand,
|
||||||
|
fetchQueueDetails,
|
||||||
|
clearQueueDetails,
|
||||||
|
fetchMovieFiles,
|
||||||
|
clearMovieFiles
|
||||||
|
};
|
||||||
|
|
||||||
|
class CutoffUnmetConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
useCurrentPage,
|
||||||
|
fetchCutoffUnmet,
|
||||||
|
gotoCutoffUnmetFirstPage
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
|
||||||
|
|
||||||
|
if (useCurrentPage) {
|
||||||
|
fetchCutoffUnmet();
|
||||||
|
} else {
|
||||||
|
gotoCutoffUnmetFirstPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||||
|
const movieIds = selectUniqueIds(this.props.items, 'id');
|
||||||
|
const movieFileIds = selectUniqueIds(this.props.items, 'movieFileId');
|
||||||
|
|
||||||
|
this.props.fetchQueueDetails({ movieIds });
|
||||||
|
|
||||||
|
if (movieFileIds.length) {
|
||||||
|
this.props.fetchMovieFiles({ movieFileIds });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
unregisterPagePopulator(this.repopulate);
|
||||||
|
this.props.clearCutoffUnmet();
|
||||||
|
this.props.clearQueueDetails();
|
||||||
|
this.props.clearMovieFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
repopulate = () => {
|
||||||
|
this.props.fetchCutoffUnmet();
|
||||||
|
};
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onFirstPagePress = () => {
|
||||||
|
this.props.gotoCutoffUnmetFirstPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onPreviousPagePress = () => {
|
||||||
|
this.props.gotoCutoffUnmetPreviousPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onNextPagePress = () => {
|
||||||
|
this.props.gotoCutoffUnmetNextPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onLastPagePress = () => {
|
||||||
|
this.props.gotoCutoffUnmetLastPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onPageSelect = (page) => {
|
||||||
|
this.props.gotoCutoffUnmetPage({ page });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSortPress = (sortKey) => {
|
||||||
|
this.props.setCutoffUnmetSort({ sortKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
|
this.props.setCutoffUnmetFilter({ selectedFilterKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
onTableOptionChange = (payload) => {
|
||||||
|
this.props.setCutoffUnmetTableOption(payload);
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
this.props.gotoCutoffUnmetFirstPage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchSelectedPress = (selected) => {
|
||||||
|
this.props.executeCommand({
|
||||||
|
name: commandNames.MOVIE_SEARCH,
|
||||||
|
movieIds: selected
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchAllCutoffUnmetPress = (monitored) => {
|
||||||
|
this.props.executeCommand({
|
||||||
|
name: commandNames.CUTOFF_UNMET_MOVIES_SEARCH,
|
||||||
|
monitored
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<CutoffUnmet
|
||||||
|
onFirstPagePress={this.onFirstPagePress}
|
||||||
|
onPreviousPagePress={this.onPreviousPagePress}
|
||||||
|
onNextPagePress={this.onNextPagePress}
|
||||||
|
onLastPagePress={this.onLastPagePress}
|
||||||
|
onPageSelect={this.onPageSelect}
|
||||||
|
onSortPress={this.onSortPress}
|
||||||
|
onFilterSelect={this.onFilterSelect}
|
||||||
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
|
onSearchSelectedPress={this.onSearchSelectedPress}
|
||||||
|
onToggleSelectedPress={this.onToggleSelectedPress}
|
||||||
|
onSearchAllCutoffUnmetPress={this.onSearchAllCutoffUnmetPress}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CutoffUnmetConnector.propTypes = {
|
||||||
|
useCurrentPage: PropTypes.bool.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
fetchCutoffUnmet: PropTypes.func.isRequired,
|
||||||
|
gotoCutoffUnmetFirstPage: PropTypes.func.isRequired,
|
||||||
|
gotoCutoffUnmetPreviousPage: PropTypes.func.isRequired,
|
||||||
|
gotoCutoffUnmetNextPage: PropTypes.func.isRequired,
|
||||||
|
gotoCutoffUnmetLastPage: PropTypes.func.isRequired,
|
||||||
|
gotoCutoffUnmetPage: PropTypes.func.isRequired,
|
||||||
|
setCutoffUnmetSort: PropTypes.func.isRequired,
|
||||||
|
setCutoffUnmetFilter: PropTypes.func.isRequired,
|
||||||
|
setCutoffUnmetTableOption: PropTypes.func.isRequired,
|
||||||
|
clearCutoffUnmet: PropTypes.func.isRequired,
|
||||||
|
executeCommand: PropTypes.func.isRequired,
|
||||||
|
fetchQueueDetails: PropTypes.func.isRequired,
|
||||||
|
clearQueueDetails: PropTypes.func.isRequired,
|
||||||
|
fetchMovieFiles: PropTypes.func.isRequired,
|
||||||
|
clearMovieFiles: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withCurrentPage(
|
||||||
|
connect(createMapStateToProps, mapDispatchToProps)(CutoffUnmetConnector)
|
||||||
|
);
|
|
@ -0,0 +1,6 @@
|
||||||
|
.languages,
|
||||||
|
.status {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 100px;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'languages': string;
|
||||||
|
'status': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
|
@ -0,0 +1,120 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import movieEntities from 'Movie/movieEntities';
|
||||||
|
import MovieSearchCellConnector from 'Movie/MovieSearchCellConnector';
|
||||||
|
import MovieStatusConnector from 'Movie/MovieStatusConnector';
|
||||||
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
|
import MovieFileLanguageConnector from 'MovieFile/MovieFileLanguageConnector';
|
||||||
|
import styles from './CutoffUnmetRow.css';
|
||||||
|
|
||||||
|
function CutoffUnmetRow(props) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
movieFileId,
|
||||||
|
year,
|
||||||
|
title,
|
||||||
|
titleSlug,
|
||||||
|
isSelected,
|
||||||
|
columns,
|
||||||
|
onSelectedChange
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
columns.map((column) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
isVisible
|
||||||
|
} = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movieMetadata.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieTitleLink
|
||||||
|
titleSlug={titleSlug}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movieMetadata.year') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{year}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'languages') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.languages}
|
||||||
|
>
|
||||||
|
<MovieFileLanguageConnector
|
||||||
|
movieFileId={movieFileId}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'status') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.status}
|
||||||
|
>
|
||||||
|
<MovieStatusConnector
|
||||||
|
movieId={id}
|
||||||
|
movieFileId={movieFileId}
|
||||||
|
movieEntity={movieEntities.WANTED_CUTOFF_UNMET}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<MovieSearchCellConnector
|
||||||
|
key={name}
|
||||||
|
movieId={id}
|
||||||
|
movieTitle={title}
|
||||||
|
movieEntity={movieEntities.WANTED_CUTOFF_UNMET}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CutoffUnmetRow.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
movieFileId: PropTypes.number,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
year: PropTypes.number.isRequired,
|
||||||
|
titleSlug: PropTypes.string.isRequired,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CutoffUnmetRow;
|
|
@ -0,0 +1,319 @@
|
||||||
|
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 ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
|
import getFilterValue from 'Utilities/Filter/getFilterValue';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
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 MissingRow from './MissingRow';
|
||||||
|
|
||||||
|
function getMonitoredValue(props) {
|
||||||
|
const {
|
||||||
|
filters,
|
||||||
|
selectedFilterKey
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return getFilterValue(filters, selectedFilterKey, 'monitored', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
class Missing extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: false,
|
||||||
|
lastToggled: null,
|
||||||
|
selectedState: {},
|
||||||
|
isConfirmSearchAllMissingModalOpen: false,
|
||||||
|
isInteractiveImportModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||||
|
this.setState((state) => {
|
||||||
|
return removeOldSelectedState(state, prevProps.items);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
getSelectedIds = () => {
|
||||||
|
return getSelectedIds(this.state.selectedState);
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onSelectAllChange = ({ value }) => {
|
||||||
|
this.setState(selectAll(this.state.selectedState, value));
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||||
|
this.setState((state) => {
|
||||||
|
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchSelectedPress = () => {
|
||||||
|
const selected = this.getSelectedIds();
|
||||||
|
|
||||||
|
this.props.onSearchSelectedPress(selected);
|
||||||
|
};
|
||||||
|
|
||||||
|
onToggleSelectedPress = () => {
|
||||||
|
const movieIds = this.getSelectedIds();
|
||||||
|
|
||||||
|
this.props.batchToggleMissingMovies({
|
||||||
|
movieIds,
|
||||||
|
monitored: !getMonitoredValue(this.props)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchAllMissingPress = () => {
|
||||||
|
this.setState({ isConfirmSearchAllMissingModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchAllMissingConfirmed = () => {
|
||||||
|
const {
|
||||||
|
selectedFilterKey,
|
||||||
|
onSearchAllMissingPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
// TODO: Custom filters will need to check whether there is a monitored
|
||||||
|
// filter once implemented.
|
||||||
|
|
||||||
|
onSearchAllMissingPress(selectedFilterKey === 'monitored');
|
||||||
|
this.setState({ isConfirmSearchAllMissingModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onConfirmSearchAllMissingModalClose = () => {
|
||||||
|
this.setState({ isConfirmSearchAllMissingModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onInteractiveImportPress = () => {
|
||||||
|
this.setState({ isInteractiveImportModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onInteractiveImportModalClose = () => {
|
||||||
|
this.setState({ isInteractiveImportModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
columns,
|
||||||
|
totalRecords,
|
||||||
|
isSearchingForMissingMovies,
|
||||||
|
isSaving,
|
||||||
|
onFilterSelect,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
selectedState,
|
||||||
|
isConfirmSearchAllMissingModalOpen,
|
||||||
|
isInteractiveImportModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const itemsSelected = !!this.getSelectedIds().length;
|
||||||
|
const isShowingMonitored = getMonitoredValue(this.props);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('Missing')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('SearchSelected')}
|
||||||
|
iconName={icons.SEARCH}
|
||||||
|
isDisabled={!itemsSelected || isSearchingForMissingMovies}
|
||||||
|
onPress={this.onSearchSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
|
||||||
|
iconName={icons.MONITORED}
|
||||||
|
isDisabled={!itemsSelected}
|
||||||
|
isSpinning={isSaving}
|
||||||
|
onPress={this.onToggleSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('SearchAll')}
|
||||||
|
iconName={icons.SEARCH}
|
||||||
|
isDisabled={!items.length}
|
||||||
|
isSpinning={isSearchingForMissingMovies}
|
||||||
|
onPress={this.onSearchAllMissingPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('ManualImport')}
|
||||||
|
iconName={icons.INTERACTIVE}
|
||||||
|
onPress={this.onInteractiveImportPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
{...otherProps}
|
||||||
|
columns={columns}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={[]}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
{
|
||||||
|
isFetching && !isPopulated &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && error &&
|
||||||
|
<Alert kind={kinds.DANGER}>
|
||||||
|
{translate('MissingLoadError')}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error && !items.length &&
|
||||||
|
<Alert kind={kinds.INFO}>
|
||||||
|
{translate('MissingNoItems')}
|
||||||
|
</Alert>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error && !!items.length &&
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
selectAll={true}
|
||||||
|
allSelected={allSelected}
|
||||||
|
allUnselected={allUnselected}
|
||||||
|
{...otherProps}
|
||||||
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<MissingRow
|
||||||
|
key={item.id}
|
||||||
|
isSelected={selectedState[item.id]}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
onSelectedChange={this.onSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmSearchAllMissingModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('SearchForAllMissingMovies')}
|
||||||
|
message={
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{translate('SearchForAllMissingMoviesConfirmationCount', { totalRecords })}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{translate('MassSearchCancelWarning')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
confirmLabel={translate('Search')}
|
||||||
|
onConfirm={this.onSearchAllMissingConfirmed}
|
||||||
|
onCancel={this.onConfirmSearchAllMissingModalClose}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<InteractiveImportModal
|
||||||
|
isOpen={isInteractiveImportModalOpen}
|
||||||
|
onModalClose={this.onInteractiveImportModalClose}
|
||||||
|
/>
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Missing.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
totalRecords: PropTypes.number,
|
||||||
|
isSearchingForMissingMovies: PropTypes.bool.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
|
onSearchSelectedPress: PropTypes.func.isRequired,
|
||||||
|
batchToggleMissingMovies: PropTypes.func.isRequired,
|
||||||
|
onSearchAllMissingPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Missing;
|
|
@ -0,0 +1,173 @@
|
||||||
|
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 withCurrentPage from 'Components/withCurrentPage';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||||
|
import * as wantedActions from 'Store/Actions/wantedActions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
import Missing from './Missing';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.wanted.missing,
|
||||||
|
createCommandExecutingSelector(commandNames.MISSING_MOVIES_SEARCH),
|
||||||
|
(missing, isSearchingForMissingMovies) => {
|
||||||
|
return {
|
||||||
|
isSearchingForMissingMovies,
|
||||||
|
isSaving: missing.items.filter((m) => m.isSaving).length > 1,
|
||||||
|
...missing
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
...wantedActions,
|
||||||
|
executeCommand,
|
||||||
|
fetchQueueDetails,
|
||||||
|
clearQueueDetails
|
||||||
|
};
|
||||||
|
|
||||||
|
class MissingConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
useCurrentPage,
|
||||||
|
fetchMissing,
|
||||||
|
gotoMissingFirstPage
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
|
||||||
|
|
||||||
|
if (useCurrentPage) {
|
||||||
|
fetchMissing();
|
||||||
|
} else {
|
||||||
|
gotoMissingFirstPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||||
|
const movieIds = selectUniqueIds(this.props.items, 'id');
|
||||||
|
this.props.fetchQueueDetails({ movieIds });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
unregisterPagePopulator(this.repopulate);
|
||||||
|
this.props.clearMissing();
|
||||||
|
this.props.clearQueueDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
repopulate = () => {
|
||||||
|
this.props.fetchMissing();
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onFirstPagePress = () => {
|
||||||
|
this.props.gotoMissingFirstPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onPreviousPagePress = () => {
|
||||||
|
this.props.gotoMissingPreviousPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onNextPagePress = () => {
|
||||||
|
this.props.gotoMissingNextPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onLastPagePress = () => {
|
||||||
|
this.props.gotoMissingLastPage();
|
||||||
|
};
|
||||||
|
|
||||||
|
onPageSelect = (page) => {
|
||||||
|
this.props.gotoMissingPage({ page });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSortPress = (sortKey) => {
|
||||||
|
this.props.setMissingSort({ sortKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
|
this.props.setMissingFilter({ selectedFilterKey });
|
||||||
|
};
|
||||||
|
|
||||||
|
onTableOptionChange = (payload) => {
|
||||||
|
this.props.setMissingTableOption(payload);
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
this.props.gotoMissingFirstPage();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchSelectedPress = (selected) => {
|
||||||
|
this.props.executeCommand({
|
||||||
|
name: commandNames.MOVIE_SEARCH,
|
||||||
|
movieIds: selected
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSearchAllMissingPress = (monitored) => {
|
||||||
|
this.props.executeCommand({
|
||||||
|
name: commandNames.MISSING_MOVIES_SEARCH,
|
||||||
|
monitored
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Missing
|
||||||
|
onFirstPagePress={this.onFirstPagePress}
|
||||||
|
onPreviousPagePress={this.onPreviousPagePress}
|
||||||
|
onNextPagePress={this.onNextPagePress}
|
||||||
|
onLastPagePress={this.onLastPagePress}
|
||||||
|
onPageSelect={this.onPageSelect}
|
||||||
|
onSortPress={this.onSortPress}
|
||||||
|
onFilterSelect={this.onFilterSelect}
|
||||||
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
|
onSearchSelectedPress={this.onSearchSelectedPress}
|
||||||
|
onSearchAllMissingPress={this.onSearchAllMissingPress}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MissingConnector.propTypes = {
|
||||||
|
useCurrentPage: PropTypes.bool.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
fetchMissing: PropTypes.func.isRequired,
|
||||||
|
gotoMissingFirstPage: PropTypes.func.isRequired,
|
||||||
|
gotoMissingPreviousPage: PropTypes.func.isRequired,
|
||||||
|
gotoMissingNextPage: PropTypes.func.isRequired,
|
||||||
|
gotoMissingLastPage: PropTypes.func.isRequired,
|
||||||
|
gotoMissingPage: PropTypes.func.isRequired,
|
||||||
|
setMissingSort: PropTypes.func.isRequired,
|
||||||
|
setMissingFilter: PropTypes.func.isRequired,
|
||||||
|
setMissingTableOption: PropTypes.func.isRequired,
|
||||||
|
clearMissing: PropTypes.func.isRequired,
|
||||||
|
executeCommand: PropTypes.func.isRequired,
|
||||||
|
fetchQueueDetails: PropTypes.func.isRequired,
|
||||||
|
clearQueueDetails: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withCurrentPage(
|
||||||
|
connect(createMapStateToProps, mapDispatchToProps)(MissingConnector)
|
||||||
|
);
|
|
@ -0,0 +1,5 @@
|
||||||
|
.status {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 100px;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'status': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
|
@ -0,0 +1,110 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import movieEntities from 'Movie/movieEntities';
|
||||||
|
import MovieSearchCellConnector from 'Movie/MovieSearchCellConnector';
|
||||||
|
import MovieStatusConnector from 'Movie/MovieStatusConnector';
|
||||||
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
|
import styles from './MissingRow.css';
|
||||||
|
|
||||||
|
function MissingRow(props) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
movieFileId,
|
||||||
|
year,
|
||||||
|
title,
|
||||||
|
titleSlug,
|
||||||
|
isSelected,
|
||||||
|
columns,
|
||||||
|
onSelectedChange
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
columns.map((column) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
isVisible
|
||||||
|
} = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movieMetadata.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieTitleLink
|
||||||
|
titleSlug={titleSlug}
|
||||||
|
title={title}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movieMetadata.year') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{year}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'status') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.status}
|
||||||
|
>
|
||||||
|
<MovieStatusConnector
|
||||||
|
movieId={id}
|
||||||
|
movieFileId={movieFileId}
|
||||||
|
movieEntity={movieEntities.WANTED_MISSING}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<MovieSearchCellConnector
|
||||||
|
key={name}
|
||||||
|
movieId={id}
|
||||||
|
movieTitle={title}
|
||||||
|
movieEntity={movieEntities.WANTED_MISSING}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MissingRow.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
movieFileId: PropTypes.number,
|
||||||
|
title: PropTypes.string.isRequired,
|
||||||
|
year: PropTypes.number.isRequired,
|
||||||
|
titleSlug: PropTypes.string.isRequired,
|
||||||
|
isSelected: PropTypes.bool,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MissingRow;
|
|
@ -57,8 +57,8 @@
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background-color: #f5f7fa;
|
background-color: var(--pageBackground);
|
||||||
color: #656565;
|
color: var(--textColor);
|
||||||
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
|
font-family: "Roboto", "open sans", "Helvetica Neue", Helvetica, Arial,
|
||||||
sans-serif;
|
sans-serif;
|
||||||
}
|
}
|
||||||
|
@ -88,14 +88,14 @@
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
border-top-right-radius: 4px;
|
border-top-right-radius: 4px;
|
||||||
background-color: #464b51;
|
background-color: var(--themeDarkColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-body {
|
.panel-body {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
border-bottom-right-radius: 4px;
|
border-bottom-right-radius: 4px;
|
||||||
border-bottom-left-radius: 4px;
|
border-bottom-left-radius: 4px;
|
||||||
background-color: #fff;
|
background-color: var(--panelBackground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sign-in {
|
.sign-in {
|
||||||
|
@ -112,16 +112,18 @@
|
||||||
padding: 6px 16px;
|
padding: 6px 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 35px;
|
height: 35px;
|
||||||
border: 1px solid #dde6e9;
|
background-color: var(--inputBackgroundColor);
|
||||||
|
border: 1px solid var(--inputBorderColor);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor);
|
||||||
|
color: var(--textColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-input:focus {
|
.form-input:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
border-color: #66afe9;
|
border-color: var(--inputFocusBorderColor);
|
||||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075),
|
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor),
|
||||||
0 0 8px rgba(102, 175, 233, 0.6);
|
0 0 8px var(--inputFocusBoxShadowColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
|
@ -130,10 +132,10 @@
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid;
|
border: 1px solid;
|
||||||
border-color: #5899eb;
|
border-color: var(--primaryBorderColor);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #5d9cec;
|
background-color: var(--primaryBackgroundColor);
|
||||||
color: #fff;
|
color: var(--white);
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
@ -141,9 +143,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
border-color: #3483e7;
|
border-color: var(--primaryHoverBorderColor);
|
||||||
background-color: #4b91ea;
|
background-color: var(--primaryHoverBackgroundColor);
|
||||||
color: #fff;
|
color: var(--white);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,24 +167,24 @@
|
||||||
|
|
||||||
.forgot-password {
|
.forgot-password {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
color: #909fa7;
|
color: var(--forgotPasswordColor);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forgot-password:focus,
|
.forgot-password:focus,
|
||||||
.forgot-password:hover {
|
.forgot-password:hover {
|
||||||
color: #748690;
|
color: var(--forgotPasswordAltColor);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forgot-password:visited {
|
.forgot-password:visited {
|
||||||
color: #748690;
|
color: var(--forgotPasswordAltColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-failed {
|
.login-failed {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
color: #f05050;
|
color: var(--failedColor);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -291,5 +293,59 @@
|
||||||
|
|
||||||
loginFailedDiv.classList.remove("hidden");
|
loginFailedDiv.classList.remove("hidden");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var light = {
|
||||||
|
white: '#fff',
|
||||||
|
pageBackground: '#f5f7fa',
|
||||||
|
textColor: '#515253',
|
||||||
|
themeDarkColor: '#464b51',
|
||||||
|
panelBackground: '#fff',
|
||||||
|
inputBackgroundColor: '#fff',
|
||||||
|
inputBorderColor: '#dde6e9',
|
||||||
|
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
|
||||||
|
inputFocusBorderColor: '#66afe9',
|
||||||
|
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
|
||||||
|
primaryBackgroundColor: '#5d9cec',
|
||||||
|
primaryBorderColor: '#5899eb',
|
||||||
|
primaryHoverBackgroundColor: '#4b91ea',
|
||||||
|
primaryHoverBorderColor: '#3483e7',
|
||||||
|
failedColor: '#f05050',
|
||||||
|
forgotPasswordColor: '#909fa7',
|
||||||
|
forgotPasswordAltColor: '#748690'
|
||||||
|
};
|
||||||
|
|
||||||
|
var dark = {
|
||||||
|
white: '#fff',
|
||||||
|
pageBackground: '#202020',
|
||||||
|
textColor: '#ccc',
|
||||||
|
themeDarkColor: '#494949',
|
||||||
|
panelBackground: '#111',
|
||||||
|
inputBackgroundColor: '#333',
|
||||||
|
inputBorderColor: '#dde6e9',
|
||||||
|
inputBoxShadowColor: 'rgba(0, 0, 0, 0.075)',
|
||||||
|
inputFocusBorderColor: '#66afe9',
|
||||||
|
inputFocusBoxShadowColor: 'rgba(102, 175, 233, 0.6)',
|
||||||
|
primaryBackgroundColor: '#5d9cec',
|
||||||
|
primaryBorderColor: '#5899eb',
|
||||||
|
primaryHoverBackgroundColor: '#4b91ea',
|
||||||
|
primaryHoverBorderColor: '#3483e7',
|
||||||
|
failedColor: '#f05050',
|
||||||
|
forgotPasswordColor: '#737d83',
|
||||||
|
forgotPasswordAltColor: '#546067'
|
||||||
|
};
|
||||||
|
|
||||||
|
var theme = "_THEME_";
|
||||||
|
var defaultDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
var finalTheme = theme === 'dark' || (theme === 'auto' && defaultDark) ?
|
||||||
|
dark :
|
||||||
|
light;
|
||||||
|
|
||||||
|
Object.entries(finalTheme).forEach(([key, value]) => {
|
||||||
|
document.documentElement.style.setProperty(
|
||||||
|
`--${key}`,
|
||||||
|
value
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
|
|
||||||
|
interface Blocklist extends ModelBase {
|
||||||
|
languages: Language[];
|
||||||
|
quality: QualityModel;
|
||||||
|
customFormats: CustomFormat[];
|
||||||
|
title: string;
|
||||||
|
date?: string;
|
||||||
|
protocol: string;
|
||||||
|
movieId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Blocklist;
|
|
@ -1,5 +1,5 @@
|
||||||
export interface UiSettings {
|
export interface UiSettings {
|
||||||
theme: string;
|
theme: 'auto' | 'dark' | 'light';
|
||||||
showRelativeDates: boolean;
|
showRelativeDates: boolean;
|
||||||
shortDateFormat: string;
|
shortDateFormat: string;
|
||||||
longDateFormat: string;
|
longDateFormat: string;
|
||||||
|
|
42
package.json
42
package.json
|
@ -29,11 +29,11 @@
|
||||||
"@fortawesome/react-fontawesome": "0.2.0",
|
"@fortawesome/react-fontawesome": "0.2.0",
|
||||||
"@juggle/resize-observer": "3.4.0",
|
"@juggle/resize-observer": "3.4.0",
|
||||||
"@microsoft/signalr": "6.0.25",
|
"@microsoft/signalr": "6.0.25",
|
||||||
"@sentry/browser": "7.51.2",
|
"@sentry/browser": "7.100.0",
|
||||||
"@sentry/integrations": "7.51.2",
|
"@sentry/integrations": "7.100.0",
|
||||||
"@types/node": "18.16.8",
|
"@types/node": "18.19.31",
|
||||||
"@types/react": "18.2.6",
|
"@types/react": "18.2.79",
|
||||||
"@types/react-dom": "18.2.4",
|
"@types/react-dom": "18.2.25",
|
||||||
"classnames": "2.3.2",
|
"classnames": "2.3.2",
|
||||||
"clipboard": "2.0.11",
|
"clipboard": "2.0.11",
|
||||||
"connected-react-router": "6.9.3",
|
"connected-react-router": "6.9.3",
|
||||||
|
@ -84,16 +84,16 @@
|
||||||
"reselect": "4.1.8",
|
"reselect": "4.1.8",
|
||||||
"stacktrace-js": "2.0.2",
|
"stacktrace-js": "2.0.2",
|
||||||
"swiper": "8.3.2",
|
"swiper": "8.3.2",
|
||||||
"typescript": "4.9.5"
|
"typescript": "5.1.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.22.11",
|
"@babel/core": "7.24.4",
|
||||||
"@babel/eslint-parser": "7.22.11",
|
"@babel/eslint-parser": "7.24.1",
|
||||||
"@babel/plugin-proposal-export-default-from": "7.22.5",
|
"@babel/plugin-proposal-export-default-from": "7.24.1",
|
||||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||||
"@babel/preset-env": "7.22.14",
|
"@babel/preset-env": "7.24.4",
|
||||||
"@babel/preset-react": "7.22.5",
|
"@babel/preset-react": "7.24.1",
|
||||||
"@babel/preset-typescript": "7.22.11",
|
"@babel/preset-typescript": "7.24.1",
|
||||||
"@types/lodash": "4.14.195",
|
"@types/lodash": "4.14.195",
|
||||||
"@types/react-lazyload": "3.2.0",
|
"@types/react-lazyload": "3.2.0",
|
||||||
"@types/react-router-dom": "5.3.3",
|
"@types/react-router-dom": "5.3.3",
|
||||||
|
@ -101,31 +101,31 @@
|
||||||
"@types/react-window": "1.8.5",
|
"@types/react-window": "1.8.5",
|
||||||
"@types/redux-actions": "2.6.2",
|
"@types/redux-actions": "2.6.2",
|
||||||
"@types/webpack-livereload-plugin": "2.3.3",
|
"@types/webpack-livereload-plugin": "2.3.3",
|
||||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||||
"@typescript-eslint/parser": "5.59.5",
|
"@typescript-eslint/parser": "6.21.0",
|
||||||
"autoprefixer": "10.4.14",
|
"autoprefixer": "10.4.14",
|
||||||
"babel-loader": "9.1.3",
|
"babel-loader": "9.1.3",
|
||||||
"babel-plugin-inline-classnames": "2.0.1",
|
"babel-plugin-inline-classnames": "2.0.1",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
|
||||||
"core-js": "3.32.1",
|
"core-js": "3.37.0",
|
||||||
"css-loader": "6.7.3",
|
"css-loader": "6.7.3",
|
||||||
"css-modules-typescript-loader": "4.0.1",
|
"css-modules-typescript-loader": "4.0.1",
|
||||||
"eslint": "8.45.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-config-prettier": "8.8.0",
|
"eslint-config-prettier": "8.10.0",
|
||||||
"eslint-plugin-filenames": "1.3.2",
|
"eslint-plugin-filenames": "1.3.2",
|
||||||
"eslint-plugin-import": "2.27.5",
|
"eslint-plugin-import": "2.29.1",
|
||||||
"eslint-plugin-json": "3.1.0",
|
"eslint-plugin-json": "3.1.0",
|
||||||
"eslint-plugin-prettier": "4.2.1",
|
"eslint-plugin-prettier": "4.2.1",
|
||||||
"eslint-plugin-react": "7.32.2",
|
"eslint-plugin-react": "7.34.1",
|
||||||
"eslint-plugin-react-hooks": "4.6.0",
|
"eslint-plugin-react-hooks": "4.6.0",
|
||||||
"eslint-plugin-simple-import-sort": "10.0.0",
|
"eslint-plugin-simple-import-sort": "12.1.0",
|
||||||
"file-loader": "6.2.0",
|
"file-loader": "6.2.0",
|
||||||
"filemanager-webpack-plugin": "8.0.0",
|
"filemanager-webpack-plugin": "8.0.0",
|
||||||
"fork-ts-checker-webpack-plugin": "8.0.0",
|
"fork-ts-checker-webpack-plugin": "8.0.0",
|
||||||
"html-webpack-plugin": "5.5.3",
|
"html-webpack-plugin": "5.5.3",
|
||||||
"loader-utils": "^3.2.1",
|
"loader-utils": "^3.2.1",
|
||||||
"mini-css-extract-plugin": "2.7.6",
|
"mini-css-extract-plugin": "2.7.6",
|
||||||
"postcss": "8.4.23",
|
"postcss": "8.4.38",
|
||||||
"postcss-color-function": "4.1.0",
|
"postcss-color-function": "4.1.0",
|
||||||
"postcss-loader": "7.3.0",
|
"postcss-loader": "7.3.0",
|
||||||
"postcss-mixins": "9.0.4",
|
"postcss-mixins": "9.0.4",
|
||||||
|
|
|
@ -99,10 +99,39 @@
|
||||||
<RootNamespace Condition="'$(RadarrProject)'=='true'">$(MSBuildProjectName.Replace('Radarr','NzbDrone'))</RootNamespace>
|
<RootNamespace Condition="'$(RadarrProject)'=='true'">$(MSBuildProjectName.Replace('Radarr','NzbDrone'))</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="'$(TestProject)'!='true'">
|
||||||
|
<!-- Annotates .NET assemblies with repository information including SHA -->
|
||||||
|
<!-- Sentry uses this to link directly to GitHub at the exact version/file/line -->
|
||||||
|
<!-- This is built-in on .NET 8 and can be removed once the project is updated -->
|
||||||
|
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Sentry specific configuration: Only in Release mode -->
|
||||||
|
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||||
|
<!-- https://docs.sentry.io/platforms/dotnet/configuration/msbuild/ -->
|
||||||
|
<!-- OrgSlug, ProjectSlug and AuthToken are required.
|
||||||
|
They can be set below, via argument to 'msbuild -p:' or environment variable -->
|
||||||
|
<SentryOrg></SentryOrg>
|
||||||
|
<SentryProject></SentryProject>
|
||||||
|
<SentryUrl></SentryUrl> <!-- If empty, assumed to be sentry.io -->
|
||||||
|
<SentryAuthToken></SentryAuthToken> <!-- Use env var instead: SENTRY_AUTH_TOKEN -->
|
||||||
|
|
||||||
|
<!-- Upload PDBs to Sentry, enabling stack traces with line numbers and file paths
|
||||||
|
without the need to deploy the application with PDBs -->
|
||||||
|
<SentryUploadSymbols>true</SentryUploadSymbols>
|
||||||
|
|
||||||
|
<!-- Source Link settings -->
|
||||||
|
<!-- https://github.com/dotnet/sourcelink/blob/main/docs/README.md#publishrepositoryurl -->
|
||||||
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
|
<!-- Embeds all source code in the respective PDB. This can make it a bit bigger but since it'll be uploaded
|
||||||
|
to Sentry and not distributed to run on the server, it helps debug crashes while making releases smaller -->
|
||||||
|
<EmbedAllSources>true</EmbedAllSources>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<!-- Standard testing packages -->
|
<!-- Standard testing packages -->
|
||||||
<ItemGroup Condition="'$(TestProject)'=='true'">
|
<ItemGroup Condition="'$(TestProject)'=='true'">
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.14.0" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
|
<PackageReference Include="NunitXml.TestLogger" Version="3.0.131" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
@ -147,16 +176,46 @@
|
||||||
</Otherwise>
|
</Otherwise>
|
||||||
</Choose>
|
</Choose>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Set architecture to RuntimeInformation.ProcessArchitecture if not specified -->
|
||||||
|
<Choose>
|
||||||
|
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X64'">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture>x64</Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</When>
|
||||||
|
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'X86'">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture>x86</Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</When>
|
||||||
|
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm64'">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture>arm64</Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</When>
|
||||||
|
<When Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture)' == 'Arm'">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture>arm</Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</When>
|
||||||
|
<Otherwise>
|
||||||
|
<PropertyGroup>
|
||||||
|
<Architecture></Architecture>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Otherwise>
|
||||||
|
</Choose>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(IsWindows)' == 'true' and
|
<PropertyGroup Condition="'$(IsWindows)' == 'true' and
|
||||||
'$(RuntimeIdentifier)' == ''">
|
'$(RuntimeIdentifier)' == ''">
|
||||||
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
||||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>win-$(Architecture)</RuntimeIdentifier>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(IsLinux)' == 'true' and
|
<PropertyGroup Condition="'$(IsLinux)' == 'true' and
|
||||||
'$(RuntimeIdentifier)' == ''">
|
'$(RuntimeIdentifier)' == ''">
|
||||||
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
<_UsingDefaultRuntimeIdentifier>true</_UsingDefaultRuntimeIdentifier>
|
||||||
<RuntimeIdentifier>linux-x64</RuntimeIdentifier>
|
<RuntimeIdentifier>linux-$(Architecture)</RuntimeIdentifier>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(IsOSX)' == 'true' and
|
<PropertyGroup Condition="'$(IsOSX)' == 'true' and
|
||||||
|
|
|
@ -55,6 +55,16 @@ namespace NzbDrone.Automation.Test
|
||||||
_page.Find(By.LinkText("Blocklist")).Should().NotBeNull();
|
_page.Find(By.LinkText("Blocklist")).Should().NotBeNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void wanted_page()
|
||||||
|
{
|
||||||
|
_page.WantedNavIcon.Click();
|
||||||
|
_page.WaitForNoSpinner();
|
||||||
|
|
||||||
|
_page.Find(By.LinkText("Missing")).Should().NotBeNull();
|
||||||
|
_page.Find(By.LinkText("Cutoff Unmet")).Should().NotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void system_page()
|
public void system_page()
|
||||||
{
|
{
|
||||||
|
|
|
@ -57,6 +57,8 @@ namespace NzbDrone.Automation.Test.PageModel
|
||||||
|
|
||||||
public IWebElement ActivityNavIcon => Find(By.LinkText("Activity"));
|
public IWebElement ActivityNavIcon => Find(By.LinkText("Activity"));
|
||||||
|
|
||||||
|
public IWebElement WantedNavIcon => Find(By.LinkText("Wanted"));
|
||||||
|
|
||||||
public IWebElement SettingNavIcon => Find(By.LinkText("Settings"));
|
public IWebElement SettingNavIcon => Find(By.LinkText("Settings"));
|
||||||
|
|
||||||
public IWebElement SystemNavIcon => Find(By.PartialLinkText("System"));
|
public IWebElement SystemNavIcon => Find(By.PartialLinkText("System"));
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
using Moq;
|
using Moq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Common.Options;
|
||||||
using NzbDrone.Core.Authentication;
|
using NzbDrone.Core.Authentication;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
|
@ -43,6 +45,26 @@ namespace NzbDrone.Common.Test
|
||||||
Mocker.GetMock<IDiskProvider>()
|
Mocker.GetMock<IDiskProvider>()
|
||||||
.Setup(v => v.WriteAllText(configFile, It.IsAny<string>()))
|
.Setup(v => v.WriteAllText(configFile, It.IsAny<string>()))
|
||||||
.Callback<string, string>((p, t) => _configFileContents = t);
|
.Callback<string, string>((p, t) => _configFileContents = t);
|
||||||
|
|
||||||
|
Mocker.GetMock<IOptions<AuthOptions>>()
|
||||||
|
.Setup(v => v.Value)
|
||||||
|
.Returns(new AuthOptions());
|
||||||
|
|
||||||
|
Mocker.GetMock<IOptions<AppOptions>>()
|
||||||
|
.Setup(v => v.Value)
|
||||||
|
.Returns(new AppOptions());
|
||||||
|
|
||||||
|
Mocker.GetMock<IOptions<ServerOptions>>()
|
||||||
|
.Setup(v => v.Value)
|
||||||
|
.Returns(new ServerOptions());
|
||||||
|
|
||||||
|
Mocker.GetMock<IOptions<LogOptions>>()
|
||||||
|
.Setup(v => v.Value)
|
||||||
|
.Returns(new LogOptions());
|
||||||
|
|
||||||
|
Mocker.GetMock<IOptions<UpdateOptions>>()
|
||||||
|
.Setup(v => v.Value)
|
||||||
|
.Returns(new UpdateOptions());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
|
|
@ -4,6 +4,7 @@ using System.Linq;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Instrumentation.Sentry;
|
using NzbDrone.Common.Instrumentation.Sentry;
|
||||||
using NzbDrone.Test.Common;
|
using NzbDrone.Test.Common;
|
||||||
|
|
||||||
|
@ -43,7 +44,7 @@ namespace NzbDrone.Common.Test.InstrumentationTests
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111");
|
_subject = new SentryTarget("https://aaaaaaaaaaaaaaaaaaaaaaaaaa@sentry.io/111111", Mocker.GetMock<IAppFolderInfo>().Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)
|
private LogEventInfo GivenLogEvent(LogLevel level, Exception ex, string message)
|
||||||
|
|
|
@ -10,6 +10,7 @@ using NUnit.Framework;
|
||||||
using NzbDrone.Common.Composition.Extensions;
|
using NzbDrone.Common.Composition.Extensions;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Instrumentation.Extensions;
|
using NzbDrone.Common.Instrumentation.Extensions;
|
||||||
|
using NzbDrone.Common.Options;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.Datastore.Extensions;
|
using NzbDrone.Core.Datastore.Extensions;
|
||||||
using NzbDrone.Core.Lifecycle;
|
using NzbDrone.Core.Lifecycle;
|
||||||
|
@ -29,10 +30,16 @@ namespace NzbDrone.Common.Test
|
||||||
.AddNzbDroneLogger()
|
.AddNzbDroneLogger()
|
||||||
.AutoAddServices(Bootstrap.ASSEMBLIES)
|
.AutoAddServices(Bootstrap.ASSEMBLIES)
|
||||||
.AddDummyDatabase()
|
.AddDummyDatabase()
|
||||||
|
.AddDummyLogDatabase()
|
||||||
.AddStartupContext(new StartupContext("first", "second"));
|
.AddStartupContext(new StartupContext("first", "second"));
|
||||||
|
|
||||||
container.RegisterInstance(new Mock<IHostLifetime>().Object);
|
container.RegisterInstance(new Mock<IHostLifetime>().Object);
|
||||||
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
|
container.RegisterInstance(new Mock<IOptions<PostgresOptions>>().Object);
|
||||||
|
container.RegisterInstance(new Mock<IOptions<AppOptions>>().Object);
|
||||||
|
container.RegisterInstance(new Mock<IOptions<AuthOptions>>().Object);
|
||||||
|
container.RegisterInstance(new Mock<IOptions<ServerOptions>>().Object);
|
||||||
|
container.RegisterInstance(new Mock<IOptions<LogOptions>>().Object);
|
||||||
|
container.RegisterInstance(new Mock<IOptions<UpdateOptions>>().Object);
|
||||||
|
|
||||||
var serviceProvider = container.GetServiceProvider();
|
var serviceProvider = container.GetServiceProvider();
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||||
RegisterDebugger();
|
RegisterDebugger();
|
||||||
}
|
}
|
||||||
|
|
||||||
RegisterSentry(updateApp);
|
RegisterSentry(updateApp, appFolderInfo);
|
||||||
|
|
||||||
if (updateApp)
|
if (updateApp)
|
||||||
{
|
{
|
||||||
|
@ -62,7 +62,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||||
LogManager.ReconfigExistingLoggers();
|
LogManager.ReconfigExistingLoggers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RegisterSentry(bool updateClient)
|
private static void RegisterSentry(bool updateClient, IAppFolderInfo appFolderInfo)
|
||||||
{
|
{
|
||||||
string dsn;
|
string dsn;
|
||||||
|
|
||||||
|
@ -77,7 +77,7 @@ namespace NzbDrone.Common.Instrumentation
|
||||||
: "https://998b4673d4c849ccb5277b5966ed5bc2@sentry.servarr.com/10";
|
: "https://998b4673d4c849ccb5277b5966ed5bc2@sentry.servarr.com/10";
|
||||||
}
|
}
|
||||||
|
|
||||||
var target = new SentryTarget(dsn)
|
var target = new SentryTarget(dsn, appFolderInfo)
|
||||||
{
|
{
|
||||||
Name = "sentryTarget",
|
Name = "sentryTarget",
|
||||||
Layout = "${message}"
|
Layout = "${message}"
|
||||||
|
|
|
@ -10,6 +10,7 @@ using NLog.Common;
|
||||||
using NLog.Targets;
|
using NLog.Targets;
|
||||||
using Npgsql;
|
using Npgsql;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
using Sentry;
|
using Sentry;
|
||||||
|
|
||||||
namespace NzbDrone.Common.Instrumentation.Sentry
|
namespace NzbDrone.Common.Instrumentation.Sentry
|
||||||
|
@ -105,7 +106,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||||
public bool FilterEvents { get; set; }
|
public bool FilterEvents { get; set; }
|
||||||
public bool SentryEnabled { get; set; }
|
public bool SentryEnabled { get; set; }
|
||||||
|
|
||||||
public SentryTarget(string dsn)
|
public SentryTarget(string dsn, IAppFolderInfo appFolderInfo)
|
||||||
{
|
{
|
||||||
_sdk = SentrySdk.Init(o =>
|
_sdk = SentrySdk.Init(o =>
|
||||||
{
|
{
|
||||||
|
@ -113,9 +114,33 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||||
o.AttachStacktrace = true;
|
o.AttachStacktrace = true;
|
||||||
o.MaxBreadcrumbs = 200;
|
o.MaxBreadcrumbs = 200;
|
||||||
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
|
o.Release = $"{BuildInfo.AppName}@{BuildInfo.Release}";
|
||||||
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
o.SetBeforeSend(x => SentryCleanser.CleanseEvent(x));
|
||||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
o.SetBeforeBreadcrumb(x => SentryCleanser.CleanseBreadcrumb(x));
|
||||||
o.Environment = BuildInfo.Branch;
|
o.Environment = BuildInfo.Branch;
|
||||||
|
|
||||||
|
// Crash free run statistics (sends a ping for healthy and for crashes sessions)
|
||||||
|
o.AutoSessionTracking = true;
|
||||||
|
|
||||||
|
// Caches files in the event device is offline
|
||||||
|
// Sentry creates a 'sentry' sub directory, no need to concat here
|
||||||
|
o.CacheDirectoryPath = appFolderInfo.GetAppDataPath();
|
||||||
|
|
||||||
|
// default environment is production
|
||||||
|
if (!RuntimeInfo.IsProduction)
|
||||||
|
{
|
||||||
|
if (RuntimeInfo.IsDevelopment)
|
||||||
|
{
|
||||||
|
o.Environment = "development";
|
||||||
|
}
|
||||||
|
else if (RuntimeInfo.IsTesting)
|
||||||
|
{
|
||||||
|
o.Environment = "testing";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
o.Environment = "other";
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
InitializeScope();
|
InitializeScope();
|
||||||
|
@ -133,7 +158,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||||
{
|
{
|
||||||
SentrySdk.ConfigureScope(scope =>
|
SentrySdk.ConfigureScope(scope =>
|
||||||
{
|
{
|
||||||
scope.User = new User
|
scope.User = new SentryUser
|
||||||
{
|
{
|
||||||
Id = HashUtil.AnonymousToken()
|
Id = HashUtil.AnonymousToken()
|
||||||
};
|
};
|
||||||
|
@ -336,13 +361,21 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var level = LoggingLevelMap[logEvent.Level];
|
||||||
var sentryEvent = new SentryEvent(logEvent.Exception)
|
var sentryEvent = new SentryEvent(logEvent.Exception)
|
||||||
{
|
{
|
||||||
Level = LoggingLevelMap[logEvent.Level],
|
Level = level,
|
||||||
Logger = logEvent.LoggerName,
|
Logger = logEvent.LoggerName,
|
||||||
Message = logEvent.FormattedMessage
|
Message = logEvent.FormattedMessage
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (level is SentryLevel.Fatal && logEvent.Exception is not null)
|
||||||
|
{
|
||||||
|
// Usages of 'fatal' here indicates the process will crash. In Sentry this is represented with
|
||||||
|
// the 'unhandled' exception flag
|
||||||
|
logEvent.Exception.SetSentryMechanism("Logger.Fatal", "Logger.Fatal was called", false);
|
||||||
|
}
|
||||||
|
|
||||||
sentryEvent.SetExtras(extras);
|
sentryEvent.SetExtras(extras);
|
||||||
sentryEvent.SetFingerprint(fingerPrint);
|
sentryEvent.SetFingerprint(fingerPrint);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
namespace NzbDrone.Common.Options;
|
||||||
|
|
||||||
|
public class AppOptions
|
||||||
|
{
|
||||||
|
public string InstanceName { get; set; }
|
||||||
|
public string Theme { get; set; }
|
||||||
|
public bool? LaunchBrowser { get; set; }
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace NzbDrone.Common.Options;
|
||||||
|
|
||||||
|
public class AuthOptions
|
||||||
|
{
|
||||||
|
public string ApiKey { get; set; }
|
||||||
|
public bool? Enabled { get; set; }
|
||||||
|
public string Method { get; set; }
|
||||||
|
public string Required { get; set; }
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
namespace NzbDrone.Common.Options;
|
||||||
|
|
||||||
|
public class LogOptions
|
||||||
|
{
|
||||||
|
public string Level { get; set; }
|
||||||
|
public bool? FilterSentryEvents { get; set; }
|
||||||
|
public int? Rotate { get; set; }
|
||||||
|
public bool? Sql { get; set; }
|
||||||
|
public string ConsoleLevel { get; set; }
|
||||||
|
public bool? AnalyticsEnabled { get; set; }
|
||||||
|
public string SyslogServer { get; set; }
|
||||||
|
public int? SyslogPort { get; set; }
|
||||||
|
public string SyslogLevel { get; set; }
|
||||||
|
public bool? DbEnabled { get; set; }
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
namespace NzbDrone.Common.Options;
|
||||||
|
|
||||||
|
public class ServerOptions
|
||||||
|
{
|
||||||
|
public string UrlBase { get; set; }
|
||||||
|
public string BindAddress { get; set; }
|
||||||
|
public int? Port { get; set; }
|
||||||
|
public bool? EnableSsl { get; set; }
|
||||||
|
public int? SslPort { get; set; }
|
||||||
|
public string SslCertPath { get; set; }
|
||||||
|
public string SslCertPassword { get; set; }
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
namespace NzbDrone.Common.Options;
|
||||||
|
|
||||||
|
public class UpdateOptions
|
||||||
|
{
|
||||||
|
public string Mechanism { get; set; }
|
||||||
|
public bool? Automatically { get; set; }
|
||||||
|
public string ScriptPath { get; set; }
|
||||||
|
public string Branch { get; set; }
|
||||||
|
}
|
|
@ -10,8 +10,8 @@
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="NLog" Version="5.2.3" />
|
<PackageReference Include="NLog" Version="5.2.3" />
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.3" />
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.3.3" />
|
||||||
<PackageReference Include="Npgsql" Version="7.0.6" />
|
<PackageReference Include="Npgsql" Version="7.0.7" />
|
||||||
<PackageReference Include="Sentry" Version="3.23.1" />
|
<PackageReference Include="Sentry" Version="4.0.2" />
|
||||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||||
<PackageReference Include="System.Text.Json" Version="6.0.9" />
|
<PackageReference Include="System.Text.Json" Version="6.0.9" />
|
||||||
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.CustomFormats;
|
||||||
|
using NzbDrone.Core.Languages;
|
||||||
|
using NzbDrone.Core.Movies;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.CustomFormats.Specifications.LanguageSpecification
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class MultiLanguageFixture : CoreTest<Core.CustomFormats.LanguageSpecification>
|
||||||
|
{
|
||||||
|
private CustomFormatInput _input;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_input = new CustomFormatInput
|
||||||
|
{
|
||||||
|
MovieInfo = Builder<ParsedMovieInfo>.CreateNew().Build(),
|
||||||
|
Movie = Builder<Movie>.CreateNew().With(m => m.MovieMetadata.Value.OriginalLanguage = Language.English).Build(),
|
||||||
|
Size = 100.Megabytes(),
|
||||||
|
Languages = new List<Language>
|
||||||
|
{
|
||||||
|
Language.English,
|
||||||
|
Language.French
|
||||||
|
},
|
||||||
|
Filename = "Movie.Title.2024"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_match_one_language()
|
||||||
|
{
|
||||||
|
Subject.Value = Language.French.Id;
|
||||||
|
Subject.Negate = false;
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_match_different_language()
|
||||||
|
{
|
||||||
|
Subject.Value = Language.Spanish.Id;
|
||||||
|
Subject.Negate = false;
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_match_negated_when_one_language_matches()
|
||||||
|
{
|
||||||
|
Subject.Value = Language.French.Id;
|
||||||
|
Subject.Negate = true;
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_match_negated_when_all_languages_do_not_match()
|
||||||
|
{
|
||||||
|
Subject.Value = Language.Spanish.Id;
|
||||||
|
Subject.Negate = true;
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using NzbDrone.Core.CustomFormats;
|
||||||
|
using NzbDrone.Core.Languages;
|
||||||
|
using NzbDrone.Core.Movies;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.CustomFormats.Specifications.LanguageSpecification
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class OriginalLanguageFixture : CoreTest<Core.CustomFormats.LanguageSpecification>
|
||||||
|
{
|
||||||
|
private CustomFormatInput _input;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_input = new CustomFormatInput
|
||||||
|
{
|
||||||
|
MovieInfo = Builder<ParsedMovieInfo>.CreateNew().Build(),
|
||||||
|
Movie = Builder<Movie>.CreateNew().With(m => m.MovieMetadata.Value.OriginalLanguage = Language.English).Build(),
|
||||||
|
Size = 100.Megabytes(),
|
||||||
|
Languages = new List<Language>
|
||||||
|
{
|
||||||
|
Language.French
|
||||||
|
},
|
||||||
|
Filename = "Movie.Title.2024"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GivenLanguages(params Language[] languages)
|
||||||
|
{
|
||||||
|
_input.Languages = languages.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_match_same_single_language()
|
||||||
|
{
|
||||||
|
GivenLanguages(Language.English);
|
||||||
|
|
||||||
|
Subject.Value = Language.Original.Id;
|
||||||
|
Subject.Negate = false;
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_match_different_single_language()
|
||||||
|
{
|
||||||
|
Subject.Value = Language.Original.Id;
|
||||||
|
Subject.Negate = false;
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_not_match_negated_same_single_language()
|
||||||
|
{
|
||||||
|
GivenLanguages(Language.English);
|
||||||
|
|
||||||
|
Subject.Value = Language.Original.Id;
|
||||||
|
Subject.Negate = true;
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_input).Should().BeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void should_match_negated_different_single_language()
|
||||||
|
{
|
||||||
|
Subject.Value = Language.Original.Id;
|
||||||
|
Subject.Negate = true;
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(_input).Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue