1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-07 13:40:38 -05:00

Compare commits

..

3 Commits

Author SHA1 Message Date
Qstick
04b15f2178 Revert cover mapping for collections 2023-10-07 14:46:07 -05:00
Qstick
39d7320a75 Test mapping before collections loop 2023-10-07 13:47:46 -05:00
Qstick
207a4b19dc Test Logging 2023-10-07 13:47:46 -05:00
1477 changed files with 28713 additions and 48597 deletions

View File

@@ -1,13 +0,0 @@
// 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": {}
}

View File

@@ -1,19 +0,0 @@
// 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"]
}
}
}

View File

@@ -1,12 +0,0 @@
# 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

2
.gitignore vendored
View File

@@ -118,7 +118,6 @@ src/UI/.idea/*
node_modules/
_output*
_artifacts
_temp*
_rawPackage/
_dotTrace*
_tests/
@@ -127,7 +126,6 @@ coverage*.xml
coverage*.json
setup/Output/
*.~is
.mono
# VS outout folders
bin

View File

@@ -1,7 +0,0 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"ms-dotnettools.csdevkit",
"ms-vscode-remote.remote-containers"
]
}

26
.vscode/launch.json vendored
View File

@@ -1,26 +0,0 @@
{
"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"
}
]
}

44
.vscode/tasks.json vendored
View File

@@ -1,44 +0,0 @@
{
"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"
}
]
}

View File

@@ -1,8 +1,8 @@
# Radarr
[![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
[![Translation status](https://translate.servarr.com/widget/servarr/radarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation/docker)
[![Translated](https://translate.servarr.com/widgets/servarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation#docker)
![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Radarr/sponsors/badge.svg)](#sponsors)

View File

@@ -9,18 +9,18 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.10.4'
majorVersion: '5.0.3'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.424'
nodeVersion: '20.X'
innoVersion: '6.2.2'
dotnetVersion: '6.0.413'
nodeVersion: '16.X'
innoVersion: '6.2.0'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04'
macImage: 'macOS-12'
macImage: 'macOS-11'
trigger:
branches:
@@ -166,10 +166,10 @@ stages:
pool:
vmImage: $(imageName)
steps:
- task: UseNode@1
- task: NodeTool@0
displayName: Set Node.js version
inputs:
version: $(nodeVersion)
versionSpec: $(nodeVersion)
- checkout: self
submodules: true
fetchDepth: 1
@@ -1089,10 +1089,10 @@ stages:
pool:
vmImage: $(imageName)
steps:
- task: UseNode@1
- task: NodeTool@0
displayName: Set Node.js version
inputs:
version: $(nodeVersion)
versionSpec: $(nodeVersion)
- checkout: self
submodules: true
fetchDepth: 1
@@ -1116,7 +1116,7 @@ stages:
vmImage: ${{ variables.windowsImage }}
steps:
- checkout: self # Need history for Sonar analysis
- task: SonarCloudPrepare@2
- task: SonarCloudPrepare@1
env:
SONAR_SCANNER_OPTS: ''
inputs:
@@ -1128,7 +1128,7 @@ stages:
cliProjectName: 'RadarrUI'
cliProjectVersion: '$(radarrVersion)'
cliSources: './frontend'
- task: SonarCloudAnalyze@2
- task: SonarCloudAnalyze@1
- job: Api_Docs
displayName: API Docs
@@ -1205,7 +1205,7 @@ stages:
submodules: true
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
- task: SonarCloudPrepare@2
- task: SonarCloudPrepare@1
condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs:
SonarCloud: 'SonarCloud'
@@ -1223,21 +1223,25 @@ stages:
./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@2
- task: SonarCloudAnalyze@1
condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results
- task: reportgenerator@5
- task: reportgenerator@4
displayName: Generate Coverage Report
inputs:
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
publishCodeCoverageResults: true
- task: PublishCodeCoverageResults@1
displayName: Publish Coverage Report
inputs:
codeCoverageTool: 'cobertura'
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
reportDirectory: './CoverageResults/combined/'
- stage: Report_Out
dependsOn:
- Analyze
- Installer
- Unit_Test
- Integration
- Automation

View File

@@ -254,7 +254,7 @@ InstallInno()
ProgressStart "Installing portable Inno Setup"
rm -rf _inno
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.2}.exe"
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
mkdir _inno
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
rm innosetup.exe

10
docs.sh
View File

@@ -21,21 +21,15 @@ slnFile=src/Radarr.sln
platform=Posix
if [ "$PLATFORM" = "Windows" ]; then
application=Radarr.Console.dll
else
application=Radarr.dll
fi
dotnet clean $slnFile -c Debug
dotnet clean $slnFile -c Release
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
dotnet new tool-manifest
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
dotnet tool install --version 6.5.0 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v3 &
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v3 &
sleep 45

View File

@@ -359,16 +359,11 @@ module.exports = {
],
rules: Object.assign(typescriptEslintRecommended.rules, {
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'after-used',
argsIgnorePattern: '^_',
ignoreRestSiblings: true
}
],
'@typescript-eslint/explicit-function-return-type': 'off',
'no-shadow': 'off',
// These should be enabled after cleaning things up
'@typescript-eslint/no-unused-vars': 'warn',
'@typescript-eslint/explicit-function-return-type': 'off',
'react/prop-types': 'off',
'prettier/prettier': 'error',
'simple-import-sort/imports': [
'error',
@@ -381,41 +376,7 @@ module.exports = {
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
]
}
],
// React Hooks
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
// React
'react/function-component-definition': 'error',
'react/hook-use-state': 'error',
'react/jsx-boolean-value': ['error', 'always'],
'react/jsx-curly-brace-presence': [
'error',
{ props: 'never', children: 'never' }
],
'react/jsx-fragments': 'error',
'react/jsx-handler-names': [
'error',
{
eventHandlerPrefix: 'on',
eventHandlerPropPrefix: 'on'
}
],
'react/jsx-no-bind': ['error', { ignoreRefs: true }],
'react/jsx-no-useless-fragment': ['error', { allowExpressions: true }],
'react/jsx-pascal-case': ['error', { allowAllCaps: true }],
'react/jsx-sort-props': [
'error',
{
callbacksLast: true,
noSortAlphabetically: true,
reservedFirst: true
}
],
'react/prop-types': 'off',
'react/self-closing-comp': 'error'
]
})
},
{

View File

@@ -9,7 +9,7 @@
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit"
"source.fixAll": true
},
"typescript.preferences.quoteStyle": "single",

View File

@@ -2,8 +2,6 @@ const loose = true;
module.exports = {
plugins: [
'@babel/plugin-transform-logical-assignment-operators',
// Stage 1
'@babel/plugin-proposal-export-default-from',
['@babel/plugin-transform-optional-chaining', { loose }],

View File

@@ -66,7 +66,7 @@ module.exports = (env) => {
output: {
path: distFolder,
publicPath: '/',
filename: isProduction ? '[name]-[contenthash].js' : '[name].js',
filename: '[name]-[contenthash].js',
sourceMapFilename: '[file].map'
},
@@ -91,7 +91,7 @@ module.exports = (env) => {
new MiniCssExtractPlugin({
filename: 'Content/styles.css',
chunkFilename: isProduction ? 'Content/[id]-[chunkhash].css' : 'Content/[id].css'
chunkFilename: 'Content/[id]-[chunkhash].css'
}),
new HtmlWebpackPlugin({
@@ -201,7 +201,7 @@ module.exports = (env) => {
options: {
importLoaders: 1,
modules: {
localIdentName: isProduction ? '[name]/[local]/[hash:base64:5]' : '[name]/[local]'
localIdentName: '[name]/[local]/[hash:base64:5]'
}
}
},

View File

@@ -16,7 +16,6 @@ const mixinsFiles = [
module.exports = {
plugins: [
'autoprefixer',
['postcss-mixins', {
mixinsFiles
}],

View File

@@ -0,0 +1,236 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
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 getRemovedItems from 'Utilities/Object/getRemovedItems';
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 BlocklistRowConnector from './BlocklistRowConnector';
class Blocklist extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isConfirmRemoveModalOpen: false,
items: props.items
};
}
componentDidUpdate(prevProps) {
const {
items
} = this.props;
if (hasDifferentItems(prevProps.items, items)) {
this.setState((state) => {
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
return;
}
}
//
// 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);
});
};
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true });
};
onRemoveSelectedConfirmed = () => {
this.props.onRemoveSelected(this.getSelectedIds());
this.setState({ isConfirmRemoveModalOpen: false });
};
onConfirmRemoveModalClose = () => {
this.setState({ isConfirmRemoveModalOpen: false });
};
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
columns,
totalRecords,
isRemoving,
isClearingBlocklistExecuting,
onClearBlocklistPress,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
return (
<PageContent title={translate('Blocklist')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('RemoveSelected')}
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
onPress={this.onRemoveSelectedPress}
/>
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isSpinning={isClearingBlocklistExecuting}
onPress={onClearBlocklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>
{translate('BlocklistLoadError')}
</Alert>
}
{
isPopulated && !error && !items.length &&
<Alert kind={kinds.INFO}>
{translate('NoHistoryBlocklist')}
</Alert>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
{...otherProps}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<BlocklistRowConnector
key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
</div>
}
</PageContentBody>
<ConfirmModal
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title={translate('RemoveSelected')}
message={translate('RemoveSelectedBlocklistMessageText')}
confirmLabel={translate('RemoveSelected')}
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
/>
</PageContent>
);
}
}
Blocklist.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlocklistPress: PropTypes.func.isRequired
};
export default Blocklist;

View File

@@ -1,329 +0,0 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { SelectProvider } from 'App/SelectContext';
import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
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 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 usePaging from 'Components/Table/usePaging';
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import usePrevious from 'Helpers/Hooks/usePrevious';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { align, icons, kinds } from 'Helpers/Props';
import {
clearBlocklist,
fetchBlocklist,
gotoBlocklistPage,
removeBlocklistItems,
setBlocklistFilter,
setBlocklistSort,
setBlocklistTableOption,
} from 'Store/Actions/blocklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import { TableOptionsChangePayload } from 'typings/Table';
import {
registerPagePopulator,
unregisterPagePopulator,
} from 'Utilities/pagePopulator';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import BlocklistFilterModal from './BlocklistFilterModal';
import BlocklistRow from './BlocklistRow';
function Blocklist() {
const requestCurrentPage = useCurrentPage();
const {
isFetching,
isPopulated,
error,
items,
columns,
selectedFilterKey,
filters,
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords,
isRemoving,
} = useSelector((state: AppState) => state.blocklist);
const customFilters = useSelector(createCustomFiltersSelector('blocklist'));
const isClearingBlocklistExecuting = useSelector(
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST)
);
const dispatch = useDispatch();
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
useState(false);
const [isConfirmClearModalOpen, setIsConfirmClearModalOpen] = useState(false);
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const selectedIds = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const wasClearingBlocklistExecuting = usePrevious(
isClearingBlocklistExecuting
);
const handleSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const handleSelectedChange = useCallback(
({ id, value, shiftKey = false }: SelectStateInputProps) => {
setSelectState({
type: 'toggleSelected',
items,
id,
isSelected: value,
shiftKey,
});
},
[items, setSelectState]
);
const handleRemoveSelectedPress = useCallback(() => {
setIsConfirmRemoveModalOpen(true);
}, [setIsConfirmRemoveModalOpen]);
const handleRemoveSelectedConfirmed = useCallback(() => {
dispatch(removeBlocklistItems({ ids: selectedIds }));
setIsConfirmRemoveModalOpen(false);
}, [selectedIds, setIsConfirmRemoveModalOpen, dispatch]);
const handleConfirmRemoveModalClose = useCallback(() => {
setIsConfirmRemoveModalOpen(false);
}, [setIsConfirmRemoveModalOpen]);
const handleClearBlocklistPress = useCallback(() => {
setIsConfirmClearModalOpen(true);
}, [setIsConfirmClearModalOpen]);
const handleClearBlocklistConfirmed = useCallback(() => {
dispatch(executeCommand({ name: commandNames.CLEAR_BLOCKLIST }));
setIsConfirmClearModalOpen(false);
}, [setIsConfirmClearModalOpen, dispatch]);
const handleConfirmClearModalClose = useCallback(() => {
setIsConfirmClearModalOpen(false);
}, [setIsConfirmClearModalOpen]);
const {
handleFirstPagePress,
handlePreviousPagePress,
handleNextPagePress,
handleLastPagePress,
handlePageSelect,
} = usePaging({
page,
totalPages,
gotoPage: gotoBlocklistPage,
});
const handleFilterSelect = useCallback(
(selectedFilterKey: string) => {
dispatch(setBlocklistFilter({ selectedFilterKey }));
},
[dispatch]
);
const handleSortPress = useCallback(
(sortKey: string) => {
dispatch(setBlocklistSort({ sortKey }));
},
[dispatch]
);
const handleTableOptionChange = useCallback(
(payload: TableOptionsChangePayload) => {
dispatch(setBlocklistTableOption(payload));
if (payload.pageSize) {
dispatch(gotoBlocklistPage({ page: 1 }));
}
},
[dispatch]
);
useEffect(() => {
if (requestCurrentPage) {
dispatch(fetchBlocklist());
} else {
dispatch(gotoBlocklistPage({ page: 1 }));
}
return () => {
dispatch(clearBlocklist());
};
}, [requestCurrentPage, dispatch]);
useEffect(() => {
const repopulate = () => {
dispatch(fetchBlocklist());
};
registerPagePopulator(repopulate);
return () => {
unregisterPagePopulator(repopulate);
};
}, [dispatch]);
useEffect(() => {
if (wasClearingBlocklistExecuting && !isClearingBlocklistExecuting) {
dispatch(gotoBlocklistPage({ page: 1 }));
}
}, [isClearingBlocklistExecuting, wasClearingBlocklistExecuting, dispatch]);
return (
<SelectProvider items={items}>
<PageContent title={translate('Blocklist')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('RemoveSelected')}
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
onPress={handleRemoveSelectedPress}
/>
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isDisabled={!items.length}
isSpinning={isClearingBlocklistExecuting}
onPress={handleClearBlocklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
onTableOptionChange={handleTableOptionChange}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={BlocklistFilterModal}
onFilterSelect={handleFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
{!isFetching && !!error ? (
<Alert kind={kinds.DANGER}>{translate('BlocklistLoadError')}</Alert>
) : null}
{isPopulated && !error && !items.length ? (
<Alert kind={kinds.INFO}>
{selectedFilterKey === 'all'
? translate('NoBlocklistItems')
: translate('BlocklistFilterHasNoItems')}
</Alert>
) : null}
{isPopulated && !error && !!items.length ? (
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
onTableOptionChange={handleTableOptionChange}
onSelectAllChange={handleSelectAllChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<BlocklistRow
key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns}
{...item}
onSelectedChange={handleSelectedChange}
/>
);
})}
</TableBody>
</Table>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
/>
</div>
) : null}
</PageContentBody>
<ConfirmModal
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title={translate('RemoveSelected')}
message={translate('RemoveSelectedBlocklistMessageText')}
confirmLabel={translate('RemoveSelected')}
onConfirm={handleRemoveSelectedConfirmed}
onCancel={handleConfirmRemoveModalClose}
/>
<ConfirmModal
isOpen={isConfirmClearModalOpen}
kind={kinds.DANGER}
title={translate('ClearBlocklist')}
message={translate('ClearBlocklistMessageText')}
confirmLabel={translate('Clear')}
onConfirm={handleClearBlocklistConfirmed}
onCancel={handleConfirmClearModalClose}
/>
</PageContent>
</SelectProvider>
);
}
export default Blocklist;

View File

@@ -0,0 +1,152 @@
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 * as blocklistActions from 'Store/Actions/blocklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Blocklist from './Blocklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blocklist,
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
(blocklist, isClearingBlocklistExecuting) => {
return {
isClearingBlocklistExecuting,
...blocklist
};
}
);
}
const mapDispatchToProps = {
...blocklistActions,
executeCommand
};
class BlocklistConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchBlocklist,
gotoBlocklistFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchBlocklist();
} else {
gotoBlocklistFirstPage();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
this.props.gotoBlocklistFirstPage();
}
}
componentWillUnmount() {
this.props.clearBlocklist();
unregisterPagePopulator(this.repopulate);
}
//
// Control
repopulate = () => {
this.props.fetchBlocklist();
};
//
// Listeners
onFirstPagePress = () => {
this.props.gotoBlocklistFirstPage();
};
onPreviousPagePress = () => {
this.props.gotoBlocklistPreviousPage();
};
onNextPagePress = () => {
this.props.gotoBlocklistNextPage();
};
onLastPagePress = () => {
this.props.gotoBlocklistLastPage();
};
onPageSelect = (page) => {
this.props.gotoBlocklistPage({ page });
};
onRemoveSelected = (ids) => {
this.props.removeBlocklistItems({ ids });
};
onSortPress = (sortKey) => {
this.props.setBlocklistSort({ sortKey });
};
onTableOptionChange = (payload) => {
this.props.setBlocklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlocklistFirstPage();
}
};
onClearBlocklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
};
//
// Render
render() {
return (
<Blocklist
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlocklistPress={this.onClearBlocklistPress}
{...this.props}
/>
);
}
}
BlocklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlocklist: PropTypes.func.isRequired,
gotoBlocklistFirstPage: PropTypes.func.isRequired,
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
gotoBlocklistNextPage: PropTypes.func.isRequired,
gotoBlocklistLastPage: PropTypes.func.isRequired,
gotoBlocklistPage: PropTypes.func.isRequired,
removeBlocklistItems: PropTypes.func.isRequired,
setBlocklistSort: PropTypes.func.isRequired,
setBlocklistTableOption: PropTypes.func.isRequired,
clearBlocklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
);

View File

@@ -0,0 +1,90 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import translate from 'Utilities/String/translate';
class BlocklistDetailsModal extends Component {
//
// Render
render() {
const {
isOpen,
sourceTitle,
protocol,
indexer,
message,
onModalClose
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<ModalContent
onModalClose={onModalClose}
>
<ModalHeader>
Details
</ModalHeader>
<ModalBody>
<DescriptionList>
<DescriptionListItem
title={translate('Name')}
data={sourceTitle}
/>
<DescriptionListItem
title={translate('Protocol')}
data={protocol}
/>
{
!!message &&
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
/>
}
{
!!message &&
<DescriptionListItem
title={translate('Message')}
data={message}
/>
}
</DescriptionList>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
BlocklistDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
message: PropTypes.string,
onModalClose: PropTypes.func.isRequired
};
export default BlocklistDetailsModal;

View File

@@ -1,64 +0,0 @@
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import translate from 'Utilities/String/translate';
interface BlocklistDetailsModalProps {
isOpen: boolean;
sourceTitle: string;
protocol: DownloadProtocol;
indexer?: string;
message?: string;
onModalClose: () => void;
}
function BlocklistDetailsModal(props: BlocklistDetailsModalProps) {
const { isOpen, sourceTitle, protocol, indexer, message, onModalClose } =
props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Details</ModalHeader>
<ModalBody>
<DescriptionList>
<DescriptionListItem title={translate('Name')} data={sourceTitle} />
<DescriptionListItem
title={translate('Protocol')}
data={protocol}
/>
{message ? (
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
/>
) : null}
{message ? (
<DescriptionListItem
title={translate('Message')}
data={message}
/>
) : null}
</DescriptionList>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
export default BlocklistDetailsModal;

View File

@@ -1,54 +0,0 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { 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}
/>
);
}

View File

@@ -1,4 +1,4 @@
.languages,
.language,
.quality {
composes: cell from '~Components/Table/Cells/TableRowCell.css';

View File

@@ -3,7 +3,7 @@
interface CssExports {
'actions': string;
'indexer': string;
'languages': string;
'language': string;
'quality': string;
}
export const cssExports: CssExports;

View File

@@ -0,0 +1,213 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import translate from 'Utilities/String/translate';
import BlocklistDetailsModal from './BlocklistDetailsModal';
import styles from './BlocklistRow.css';
class BlocklistRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isDetailsModalOpen: false
};
}
//
// Listeners
onDetailsPress = () => {
this.setState({ isDetailsModalOpen: true });
};
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
};
//
// Render
render() {
const {
id,
movie,
sourceTitle,
quality,
customFormats,
languages,
date,
protocol,
indexer,
message,
isSelected,
columns,
onSelectedChange,
onRemovePress
} = this.props;
if (!movie) {
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={movie.titleSlug}
title={movie.title}
/>
</TableRowCell>
);
}
if (name === 'sourceTitle') {
return (
<TableRowCell key={name}>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguage
languages={languages}
/>
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell
key={name}
className={styles.quality}
>
<MovieQuality
quality={quality}
/>
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats
formats={customFormats}
/>
</TableRowCell>
);
}
if (name === 'date') {
return (
<RelativeDateCellConnector
key={name}
date={date}
/>
);
}
if (name === 'indexer') {
return (
<TableRowCell
key={name}
className={styles.indexer}
>
{indexer}
</TableRowCell>
);
}
if (name === 'actions') {
return (
<TableRowCell
key={name}
className={styles.actions}
>
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
/>
<IconButton
title={translate('RemoveFromBlocklist')}
name={icons.REMOVE}
kind={kinds.DANGER}
onPress={onRemovePress}
/>
</TableRowCell>
);
}
return null;
})
}
<BlocklistDetailsModal
isOpen={this.state.isDetailsModalOpen}
sourceTitle={sourceTitle}
protocol={protocol}
indexer={indexer}
message={message}
onModalClose={this.onDetailsModalClose}
/>
</TableRow>
);
}
}
BlocklistRow.propTypes = {
id: PropTypes.number.isRequired,
movie: PropTypes.object.isRequired,
sourceTitle: PropTypes.string.isRequired,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
date: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
message: PropTypes.string,
isSelected: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired
};
export default BlocklistRow;

View File

@@ -1,160 +0,0 @@
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import useMovie from 'Movie/useMovie';
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
import Blocklist from 'typings/Blocklist';
import { SelectStateInputProps } from 'typings/props';
import translate from 'Utilities/String/translate';
import BlocklistDetailsModal from './BlocklistDetailsModal';
import styles from './BlocklistRow.css';
interface BlocklistRowProps extends Blocklist {
isSelected: boolean;
columns: Column[];
onSelectedChange: (options: SelectStateInputProps) => void;
}
function BlocklistRow(props: BlocklistRowProps) {
const {
id,
movieId,
sourceTitle,
languages,
quality,
customFormats,
date,
protocol,
indexer,
message,
isSelected,
columns,
onSelectedChange,
} = props;
const movie = useMovie(movieId);
const dispatch = useDispatch();
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
const handleDetailsPress = useCallback(() => {
setIsDetailsModalOpen(true);
}, [setIsDetailsModalOpen]);
const handleDetailsModalClose = useCallback(() => {
setIsDetailsModalOpen(false);
}, [setIsDetailsModalOpen]);
const handleRemovePress = useCallback(() => {
dispatch(removeBlocklistItem({ id }));
}, [id, dispatch]);
if (!movie) {
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={movie.titleSlug} title={movie.title} />
</TableRowCell>
);
}
if (name === 'sourceTitle') {
return <TableRowCell key={name}>{sourceTitle}</TableRowCell>;
}
if (name === 'languages') {
return (
<TableRowCell key={name} className={styles.languages}>
<MovieLanguages languages={languages} />
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name} className={styles.quality}>
<MovieQuality quality={quality} />
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats formats={customFormats} />
</TableRowCell>
);
}
if (name === 'date') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
return <RelativeDateCell key={name} date={date} />;
}
if (name === 'indexer') {
return (
<TableRowCell key={name} className={styles.indexer}>
{indexer}
</TableRowCell>
);
}
if (name === 'actions') {
return (
<TableRowCell key={name} className={styles.actions}>
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
<IconButton
title={translate('RemoveFromBlocklist')}
name={icons.REMOVE}
kind={kinds.DANGER}
onPress={handleRemovePress}
/>
</TableRowCell>
);
}
return null;
})}
<BlocklistDetailsModal
isOpen={isDetailsModalOpen}
sourceTitle={sourceTitle}
protocol={protocol}
indexer={indexer}
message={message}
onModalClose={handleDetailsModalClose}
/>
</TableRow>
);
}
export default BlocklistRow;

View File

@@ -0,0 +1,26 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import BlocklistRow from './BlocklistRow';
function createMapStateToProps() {
return createSelector(
createMovieSelector(),
(movie) => {
return {
movie
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onRemovePress() {
dispatch(removeBlocklistItem({ id: props.id }));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);

View File

@@ -14,7 +14,6 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import HistoryFilterModal from './HistoryFilterModal';
import HistoryRowConnector from './HistoryRowConnector';
class History extends Component {
@@ -34,7 +33,6 @@ class History extends Component {
columns,
selectedFilterKey,
filters,
customFilters,
totalRecords,
onFilterSelect,
onFirstPagePress,
@@ -72,8 +70,7 @@ class History extends Component {
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={HistoryFilterModal}
customFilters={[]}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
@@ -147,9 +144,8 @@ History.propTypes = {
moviesError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
selectedFilterKey: PropTypes.string.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
onFilterSelect: PropTypes.func.isRequired,
onFirstPagePress: PropTypes.func.isRequired

View File

@@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import withCurrentPage from 'Components/withCurrentPage';
import * as historyActions from 'Store/Actions/historyActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import History from './History';
@@ -12,13 +11,11 @@ function createMapStateToProps() {
return createSelector(
(state) => state.history,
(state) => state.movies,
createCustomFiltersSelector('history'),
(history, movies, customFilters) => {
(history, movies) => {
return {
isMoviesFetching: movies.isFetching,
isMoviesPopulated: movies.isPopulated,
moviesError: movies.error,
customFilters,
...history
};
}

View File

@@ -6,7 +6,7 @@ import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './HistoryEventTypeCell.css';
function getIconName(eventType, data) {
function getIconName(eventType) {
switch (eventType) {
case 'grabbed':
return icons.DOWNLOADING;
@@ -17,7 +17,7 @@ function getIconName(eventType, data) {
case 'downloadFailed':
return icons.DOWNLOADING;
case 'movieFileDeleted':
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
return icons.DELETE;
case 'movieFileRenamed':
return icons.ORGANIZE;
case 'downloadIgnored':
@@ -47,7 +47,7 @@ function getTooltip(eventType, data) {
case 'downloadFailed':
return translate('MovieDownloadFailedTooltip');
case 'movieFileDeleted':
return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip');
return translate('MovieFileDeletedTooltip');
case 'movieFileRenamed':
return translate('MovieFileRenamedTooltip');
case 'downloadIgnored':
@@ -58,7 +58,7 @@ function getTooltip(eventType, data) {
}
function HistoryEventTypeCell({ eventType, data }) {
const iconName = getIconName(eventType, data);
const iconName = getIconName(eventType);
const iconKind = getIconKind(eventType);
const tooltip = getTooltip(eventType, data);

View File

@@ -1,54 +0,0 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setHistoryFilter } from 'Store/Actions/historyActions';
function createHistorySelector() {
return createSelector(
(state: AppState) => state.history.items,
(queueItems) => {
return queueItems;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.history.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface HistoryFilterModalProps {
isOpen: boolean;
}
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
const sectionItems = useSelector(createHistorySelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'history';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setHistoryFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}

View File

@@ -1,13 +1,13 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, tooltipPositions } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
@@ -113,7 +113,7 @@ class HistoryRow extends Component {
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguages
<MovieLanguage
languages={languages}
/>
</TableRowCell>
@@ -143,7 +143,7 @@ class HistoryRow extends Component {
if (name === 'date') {
return (
<RelativeDateCell
<RelativeDateCellConnector
key={name}
date={date}
/>

View File

@@ -11,7 +11,3 @@
border-color: var(--usenetColor);
background-color: var(--usenetColor);
}
.unknown {
composes: label from '~Components/Label.css';
}

View File

@@ -2,7 +2,6 @@
// Please do not change this file!
interface CssExports {
'torrent': string;
'unknown': string;
'usenet': string;
}
export const cssExports: CssExports;

View File

@@ -0,0 +1,20 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import styles from './ProtocolLabel.css';
function ProtocolLabel({ protocol }) {
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
return (
<Label className={styles[protocol]}>
{protocolName}
</Label>
);
}
ProtocolLabel.propTypes = {
protocol: PropTypes.string.isRequired
};
export default ProtocolLabel;

View File

@@ -1,16 +0,0 @@
import React from 'react';
import Label from 'Components/Label';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import styles from './ProtocolLabel.css';
interface ProtocolLabelProps {
protocol: DownloadProtocol;
}
function ProtocolLabel({ protocol }: ProtocolLabelProps) {
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
return <Label className={styles[protocol]}>{protocolName}</Label>;
}
export default ProtocolLabel;

View File

@@ -0,0 +1,337 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 getRemovedItems from 'Utilities/Object/getRemovedItems';
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 QueueOptionsConnector from './QueueOptionsConnector';
import QueueRowConnector from './QueueRowConnector';
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
class Queue extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._shouldBlockRefresh = false;
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isPendingSelected: false,
isConfirmRemoveModalOpen: false,
items: props.items
};
}
shouldComponentUpdate() {
if (this._shouldBlockRefresh) {
return false;
}
return true;
}
componentDidUpdate(prevProps) {
const {
items,
isFetching,
isMoviesFetching
} = this.props;
if (
(!isMoviesFetching && prevProps.isMoviesFetching) ||
(!isFetching && prevProps.isFetching) ||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.movieId))
) {
this.setState((state) => {
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
return;
}
const nextState = {};
if (prevProps.items !== items) {
nextState.items = items;
}
const selectedIds = this.getSelectedIds();
const isPendingSelected = _.some(this.props.items, (item) => {
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
});
if (isPendingSelected !== this.state.isPendingSelected) {
nextState.isPendingSelected = isPendingSelected;
}
if (!_.isEmpty(nextState)) {
this.setState(nextState);
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
};
//
// Listeners
onQueueRowModalOpenOrClose = (isOpen) => {
this._shouldBlockRefresh = isOpen;
};
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);
});
};
onGrabSelectedPress = () => {
this.props.onGrabSelectedPress(this.getSelectedIds());
};
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true }, () => {
this._shouldBlockRefresh = true;
});
};
onRemoveSelectedConfirmed = (payload) => {
this._shouldBlockRefresh = false;
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
this.setState({ isConfirmRemoveModalOpen: false });
};
onConfirmRemoveModalClose = () => {
this._shouldBlockRefresh = false;
this.setState({ isConfirmRemoveModalOpen: false });
};
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
isMoviesFetching,
isMoviesPopulated,
moviesError,
columns,
totalRecords,
isGrabbing,
isRemoving,
isRefreshMonitoredDownloadsExecuting,
onRefreshPress,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen,
isPendingSelected,
items
} = this.state;
const isRefreshing = isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length || items.every((e) => !e.movieId));
const hasError = error || moviesError;
const selectedIds = this.getSelectedIds();
const selectedCount = selectedIds.length;
const disableSelectedActions = selectedCount === 0;
return (
<PageContent title={translate('Queue')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Refresh')}
iconName={icons.REFRESH}
isSpinning={isRefreshing}
onPress={onRefreshPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('GrabSelected')}
iconName={icons.DOWNLOAD}
isDisabled={disableSelectedActions || !isPendingSelected}
isSpinning={isGrabbing}
onPress={this.onGrabSelectedPress}
/>
<PageToolbarButton
label={translate('RemoveSelected')}
iconName={icons.REMOVE}
isDisabled={disableSelectedActions}
isSpinning={isRemoving}
onPress={this.onRemoveSelectedPress}
/>
</PageToolbarSection>
<PageToolbarSection
alignContent={align.RIGHT}
>
<TableOptionsModalWrapper
columns={columns}
{...otherProps}
optionsComponent={QueueOptionsConnector}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isRefreshing && !isAllPopulated ?
<LoadingIndicator /> :
null
}
{
!isRefreshing && hasError ?
<Alert kind={kinds.DANGER}>
{translate('QueueLoadError')}
</Alert> :
null
}
{
isAllPopulated && !hasError && !items.length ?
<Alert kind={kinds.INFO}>
{translate('QueueIsEmpty')}
</Alert> :
null
}
{
isAllPopulated && !hasError && !!items.length ?
<div>
<Table
columns={columns}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
{...otherProps}
optionsComponent={QueueOptionsConnector}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<QueueRowConnector
key={item.id}
movieId={item.movieId}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isRefreshing}
{...otherProps}
/>
</div> :
null
}
</PageContentBody>
<RemoveQueueItemsModal
isOpen={isConfirmRemoveModalOpen}
selectedCount={selectedCount}
canIgnore={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
return !!(item && item.movieId);
})
)}
allPending={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
if (!item) {
return false;
}
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
})
)}
onRemovePress={this.onRemoveSelectedConfirmed}
onModalClose={this.onConfirmRemoveModalClose}
/>
</PageContent>
);
}
}
Queue.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
isMoviesFetching: PropTypes.bool.isRequired,
isMoviesPopulated: PropTypes.bool.isRequired,
moviesError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isGrabbing: PropTypes.bool.isRequired,
isRemoving: PropTypes.bool.isRequired,
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
onRefreshPress: PropTypes.func.isRequired,
onGrabSelectedPress: PropTypes.func.isRequired,
onRemoveSelectedPress: PropTypes.func.isRequired
};
export default Queue;

View File

@@ -1,400 +0,0 @@
import React, {
ReactElement,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import * as commandNames from 'Commands/commandNames';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
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 usePaging from 'Components/Table/usePaging';
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
import useSelectState from 'Helpers/Hooks/useSelectState';
import { align, icons, kinds } from 'Helpers/Props';
import createMoviesFetchingSelector from 'Movie/createMoviesFetchingSelector';
import { executeCommand } from 'Store/Actions/commandActions';
import {
clearQueue,
fetchQueue,
gotoQueuePage,
grabQueueItems,
removeQueueItems,
setQueueFilter,
setQueueSort,
setQueueTableOption,
} from 'Store/Actions/queueActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { CheckInputChanged } from 'typings/inputs';
import { SelectStateInputProps } from 'typings/props';
import { TableOptionsChangePayload } from 'typings/Table';
import {
registerPagePopulator,
unregisterPagePopulator,
} from 'Utilities/pagePopulator';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import QueueFilterModal from './QueueFilterModal';
import QueueOptions from './QueueOptions';
import QueueRow from './QueueRow';
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
import createQueueStatusSelector from './Status/createQueueStatusSelector';
function Queue() {
const requestCurrentPage = useCurrentPage();
const dispatch = useDispatch();
const {
isFetching,
isPopulated,
error,
items,
columns,
selectedFilterKey,
filters,
sortKey,
sortDirection,
page,
pageSize,
totalPages,
totalRecords,
isGrabbing,
isRemoving,
} = useSelector((state: AppState) => state.queue.paged);
const { count } = useSelector(createQueueStatusSelector());
const { isMoviesFetching, isMoviesPopulated, moviesError } = useSelector(
createMoviesFetchingSelector()
);
const customFilters = useSelector(createCustomFiltersSelector('queue'));
const isRefreshMonitoredDownloadsExecuting = useSelector(
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS)
);
const shouldBlockRefresh = useRef(false);
const currentQueue = useRef<ReactElement | null>(null);
const [selectState, setSelectState] = useSelectState();
const { allSelected, allUnselected, selectedState } = selectState;
const selectedIds = useMemo(() => {
return getSelectedIds(selectedState);
}, [selectedState]);
const isPendingSelected = useMemo(() => {
return items.some((item) => {
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
});
}, [items, selectedIds]);
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] =
useState(false);
const isRefreshing =
isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
const isAllPopulated =
isPopulated &&
(isMoviesPopulated || !items.length || items.every((m) => !m.movieId));
const hasError = error || moviesError;
const selectedCount = selectedIds.length;
const disableSelectedActions = selectedCount === 0;
const handleSelectAllChange = useCallback(
({ value }: CheckInputChanged) => {
setSelectState({ type: value ? 'selectAll' : 'unselectAll', items });
},
[items, setSelectState]
);
const handleSelectedChange = useCallback(
({ id, value, shiftKey = false }: SelectStateInputProps) => {
setSelectState({
type: 'toggleSelected',
items,
id,
isSelected: value,
shiftKey,
});
},
[items, setSelectState]
);
const handleRefreshPress = useCallback(() => {
dispatch(
executeCommand({
name: commandNames.REFRESH_MONITORED_DOWNLOADS,
})
);
}, [dispatch]);
const handleQueueRowModalOpenOrClose = useCallback((isOpen: boolean) => {
shouldBlockRefresh.current = isOpen;
}, []);
const handleGrabSelectedPress = useCallback(() => {
dispatch(grabQueueItems({ ids: selectedIds }));
}, [selectedIds, dispatch]);
const handleRemoveSelectedPress = useCallback(() => {
shouldBlockRefresh.current = true;
setIsConfirmRemoveModalOpen(true);
}, [setIsConfirmRemoveModalOpen]);
const handleRemoveSelectedConfirmed = useCallback(
(payload: RemovePressProps) => {
shouldBlockRefresh.current = false;
dispatch(removeQueueItems({ ids: selectedIds, ...payload }));
setIsConfirmRemoveModalOpen(false);
},
[selectedIds, setIsConfirmRemoveModalOpen, dispatch]
);
const handleConfirmRemoveModalClose = useCallback(() => {
shouldBlockRefresh.current = false;
setIsConfirmRemoveModalOpen(false);
}, [setIsConfirmRemoveModalOpen]);
const {
handleFirstPagePress,
handlePreviousPagePress,
handleNextPagePress,
handleLastPagePress,
handlePageSelect,
} = usePaging({
page,
totalPages,
gotoPage: gotoQueuePage,
});
const handleFilterSelect = useCallback(
(selectedFilterKey: string) => {
dispatch(setQueueFilter({ selectedFilterKey }));
},
[dispatch]
);
const handleSortPress = useCallback(
(sortKey: string) => {
dispatch(setQueueSort({ sortKey }));
},
[dispatch]
);
const handleTableOptionChange = useCallback(
(payload: TableOptionsChangePayload) => {
dispatch(setQueueTableOption(payload));
if (payload.pageSize) {
dispatch(gotoQueuePage({ page: 1 }));
}
},
[dispatch]
);
useEffect(() => {
if (requestCurrentPage) {
dispatch(fetchQueue());
} else {
dispatch(gotoQueuePage({ page: 1 }));
}
return () => {
dispatch(clearQueue());
};
}, [requestCurrentPage, dispatch]);
useEffect(() => {
const repopulate = () => {
dispatch(fetchQueue());
};
registerPagePopulator(repopulate);
return () => {
unregisterPagePopulator(repopulate);
};
}, [dispatch]);
if (!shouldBlockRefresh.current) {
currentQueue.current = (
<PageContentBody>
{isRefreshing && !isAllPopulated ? <LoadingIndicator /> : null}
{!isRefreshing && hasError ? (
<Alert kind={kinds.DANGER}>{translate('QueueLoadError')}</Alert>
) : null}
{isAllPopulated && !hasError && !items.length ? (
<Alert kind={kinds.INFO}>
{selectedFilterKey !== 'all' && count > 0
? translate('QueueFilterHasNoItems')
: translate('QueueIsEmpty')}
</Alert>
) : null}
{isAllPopulated && !hasError && !!items.length ? (
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
pageSize={pageSize}
sortKey={sortKey}
sortDirection={sortDirection}
optionsComponent={QueueOptions}
onTableOptionChange={handleTableOptionChange}
onSelectAllChange={handleSelectAllChange}
onSortPress={handleSortPress}
>
<TableBody>
{items.map((item) => {
return (
<QueueRow
key={item.id}
movieId={item.movieId}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={handleSelectedChange}
onQueueRowModalOpenOrClose={
handleQueueRowModalOpenOrClose
}
/>
);
})}
</TableBody>
</Table>
<TablePager
page={page}
totalPages={totalPages}
totalRecords={totalRecords}
isFetching={isFetching}
onFirstPagePress={handleFirstPagePress}
onPreviousPagePress={handlePreviousPagePress}
onNextPagePress={handleNextPagePress}
onLastPagePress={handleLastPagePress}
onPageSelect={handlePageSelect}
/>
</div>
) : null}
</PageContentBody>
);
}
return (
<PageContent title={translate('Queue')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Refresh"
iconName={icons.REFRESH}
isSpinning={isRefreshing}
onPress={handleRefreshPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('GrabSelected')}
iconName={icons.DOWNLOAD}
isDisabled={disableSelectedActions || !isPendingSelected}
isSpinning={isGrabbing}
onPress={handleGrabSelectedPress}
/>
<PageToolbarButton
label={translate('RemoveSelected')}
iconName={icons.REMOVE}
isDisabled={disableSelectedActions}
isSpinning={isRemoving}
onPress={handleRemoveSelectedPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
pageSize={pageSize}
maxPageSize={200}
optionsComponent={QueueOptions}
onTableOptionChange={handleTableOptionChange}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={QueueFilterModal}
onFilterSelect={handleFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
{currentQueue.current}
<RemoveQueueItemModal
isOpen={isConfirmRemoveModalOpen}
selectedCount={selectedCount}
canChangeCategory={
isConfirmRemoveModalOpen &&
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
return !!(item && item.downloadClientHasPostImportCategory);
})
}
canIgnore={
isConfirmRemoveModalOpen &&
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
return !!(item && item.movieId);
})
}
isPending={
isConfirmRemoveModalOpen &&
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
if (!item) {
return false;
}
return (
item.status === 'delay' ||
item.status === 'downloadClientUnavailable'
);
})
}
onRemovePress={handleRemoveSelectedConfirmed}
onModalClose={handleConfirmRemoveModalClose}
/>
</PageContent>
);
}
export default Queue;

View File

@@ -0,0 +1,174 @@
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 * as queueActions from 'Store/Actions/queueActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Queue from './Queue';
function createMapStateToProps() {
return createSelector(
(state) => state.movies,
(state) => state.queue.options,
(state) => state.queue.paged,
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
(movies, options, queue, isRefreshMonitoredDownloadsExecuting) => {
return {
isMoviesFetching: movies.isFetching,
isMoviesPopulated: movies.isPopulated,
moviesError: movies.error,
isRefreshMonitoredDownloadsExecuting,
...options,
...queue
};
}
);
}
const mapDispatchToProps = {
...queueActions,
executeCommand
};
class QueueConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchQueue,
fetchQueueStatus,
gotoQueueFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchQueue();
} else {
gotoQueueFirstPage();
}
fetchQueueStatus();
}
componentDidUpdate(prevProps) {
if (
this.props.includeUnknownMovieItems !==
prevProps.includeUnknownMovieItems
) {
this.repopulate();
}
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
this.props.clearQueue();
}
//
// Control
repopulate = () => {
this.props.fetchQueue();
};
//
// Listeners
onFirstPagePress = () => {
this.props.gotoQueueFirstPage();
};
onPreviousPagePress = () => {
this.props.gotoQueuePreviousPage();
};
onNextPagePress = () => {
this.props.gotoQueueNextPage();
};
onLastPagePress = () => {
this.props.gotoQueueLastPage();
};
onPageSelect = (page) => {
this.props.gotoQueuePage({ page });
};
onSortPress = (sortKey) => {
this.props.setQueueSort({ sortKey });
};
onTableOptionChange = (payload) => {
this.props.setQueueTableOption(payload);
if (payload.pageSize) {
this.props.gotoQueueFirstPage();
}
};
onRefreshPress = () => {
this.props.executeCommand({
name: commandNames.REFRESH_MONITORED_DOWNLOADS
});
};
onGrabSelectedPress = (ids) => {
this.props.grabQueueItems({ ids });
};
onRemoveSelectedPress = (payload) => {
this.props.removeQueueItems(payload);
};
//
// Render
render() {
return (
<Queue
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onRefreshPress={this.onRefreshPress}
onGrabSelectedPress={this.onGrabSelectedPress}
onRemoveSelectedPress={this.onRemoveSelectedPress}
{...this.props}
/>
);
}
}
QueueConnector.propTypes = {
includeUnknownMovieItems: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchQueue: PropTypes.func.isRequired,
fetchQueueStatus: PropTypes.func.isRequired,
gotoQueueFirstPage: PropTypes.func.isRequired,
gotoQueuePreviousPage: PropTypes.func.isRequired,
gotoQueueNextPage: PropTypes.func.isRequired,
gotoQueueLastPage: PropTypes.func.isRequired,
gotoQueuePage: PropTypes.func.isRequired,
setQueueSort: PropTypes.func.isRequired,
setQueueTableOption: PropTypes.func.isRequired,
clearQueue: PropTypes.func.isRequired,
grabQueueItems: PropTypes.func.isRequired,
removeQueueItems: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(QueueConnector)
);

View File

@@ -1,49 +1,36 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import Popover from 'Components/Tooltip/Popover';
import { icons, tooltipPositions } from 'Helpers/Props';
import {
QueueTrackedDownloadState,
QueueTrackedDownloadStatus,
StatusMessage,
} from 'typings/Queue';
import translate from 'Utilities/String/translate';
import QueueStatus from './QueueStatus';
import styles from './QueueDetails.css';
interface QueueDetailsProps {
title: string;
size: number;
sizeleft: number;
estimatedCompletionTime?: string;
status: string;
trackedDownloadState?: QueueTrackedDownloadState;
trackedDownloadStatus?: QueueTrackedDownloadStatus;
statusMessages?: StatusMessage[];
errorMessage?: string;
progressBar: React.ReactNode;
}
function QueueDetails(props: QueueDetailsProps) {
function QueueDetails(props) {
const {
title,
size,
sizeleft,
status,
trackedDownloadState = 'downloading',
trackedDownloadStatus = 'ok',
trackedDownloadState,
trackedDownloadStatus,
statusMessages,
errorMessage,
progressBar,
progressBar
} = props;
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
const progress = size ? (100 - sizeleft / size * 100) : 0;
const isDownloading = status === 'downloading';
const isPaused = status === 'paused';
const hasWarning = trackedDownloadStatus === 'warning';
const hasError = trackedDownloadStatus === 'error';
if ((isDownloading || isPaused) && !hasWarning && !hasError) {
if (
(isDownloading || isPaused) &&
!hasWarning &&
!hasError
) {
const state = isPaused ? translate('Paused') : translate('Downloading');
if (progress < 5) {
@@ -58,9 +45,11 @@ function QueueDetails(props: QueueDetailsProps) {
return (
<Popover
className={styles.progressBarContainer}
anchor={progressBar!}
anchor={progressBar}
title={`${state} - ${progress.toFixed(1)}%`}
body={<div>{title}</div>}
body={
<div>{title}</div>
}
position={tooltipPositions.LEFT}
/>
);
@@ -79,4 +68,17 @@ function QueueDetails(props: QueueDetailsProps) {
);
}
QueueDetails.propTypes = {
title: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string,
progressBar: PropTypes.node.isRequired
};
export default QueueDetails;

View File

@@ -1,54 +0,0 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setQueueFilter } from 'Store/Actions/queueActions';
function createQueueSelector() {
return createSelector(
(state: AppState) => state.queue.paged.items,
(queueItems) => {
return queueItems;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.queue.paged.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface QueueFilterModalProps {
isOpen: boolean;
}
export default function QueueFilterModal(props: QueueFilterModalProps) {
const sectionItems = useSelector(createQueueSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'queue';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setQueueFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}

View File

@@ -0,0 +1,78 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class QueueOptions extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
includeUnknownMovieItems: props.includeUnknownMovieItems
};
}
componentDidUpdate(prevProps) {
const {
includeUnknownMovieItems
} = this.props;
if (includeUnknownMovieItems !== prevProps.includeUnknownMovieItems) {
this.setState({
includeUnknownMovieItems
});
}
}
//
// Listeners
onOptionChange = ({ name, value }) => {
this.setState({
[name]: value
}, () => {
this.props.onOptionChange({
[name]: value
});
});
};
//
// Render
render() {
const {
includeUnknownMovieItems
} = this.state;
return (
<Fragment>
<FormGroup>
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="includeUnknownMovieItems"
value={includeUnknownMovieItems}
helpText={translate('ShowUnknownMovieItemsHelpText')}
onChange={this.onOptionChange}
/>
</FormGroup>
</Fragment>
);
}
}
QueueOptions.propTypes = {
includeUnknownMovieItems: PropTypes.bool.isRequired,
onOptionChange: PropTypes.func.isRequired
};
export default QueueOptions;

View File

@@ -1,48 +0,0 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
import { CheckInputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
function QueueOptions() {
const dispatch = useDispatch();
const { includeUnknownMovieItems } = useSelector(
(state: AppState) => state.queue.options
);
const handleOptionChange = useCallback(
({ name, value }: CheckInputChanged) => {
dispatch(
setQueueOption({
[name]: value,
})
);
if (name === 'includeUnknownMovieItems') {
dispatch(gotoQueuePage({ page: 1 }));
}
},
[dispatch]
);
return (
<FormGroup>
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="includeUnknownMovieItems"
value={includeUnknownMovieItems}
helpText={translate('ShowUnknownMovieItemsHelpText')}
onChange={handleOptionChange}
/>
</FormGroup>
);
}
export default QueueOptions;

View File

@@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setQueueOption } from 'Store/Actions/queueActions';
import QueueOptions from './QueueOptions';
function createMapStateToProps() {
return createSelector(
(state) => state.queue.options,
(options) => {
return options;
}
);
}
const mapDispatchToProps = {
onOptionChange: setQueueOption
};
export default connect(createMapStateToProps, mapDispatchToProps)(QueueOptions);

View File

@@ -26,5 +26,4 @@
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px;
text-align: right;
}

View File

@@ -0,0 +1,420 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar';
// import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import QueueStatusCell from './QueueStatusCell';
import RemoveQueueItemModal from './RemoveQueueItemModal';
import TimeleftCell from './TimeleftCell';
import styles from './QueueRow.css';
class QueueRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isRemoveQueueItemModalOpen: false,
isInteractiveImportModalOpen: false
};
}
//
// Listeners
onRemoveQueueItemPress = () => {
this.setState({ isRemoveQueueItemModalOpen: true });
};
onRemoveQueueItemModalConfirmed = (blocklist, skipRedownload) => {
const {
onRemoveQueueItemPress,
onQueueRowModalOpenOrClose
} = this.props;
onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blocklist, skipRedownload);
this.setState({ isRemoveQueueItemModalOpen: false });
};
onRemoveQueueItemModalClose = () => {
this.props.onQueueRowModalOpenOrClose(false);
this.setState({ isRemoveQueueItemModalOpen: false });
};
onInteractiveImportPress = () => {
this.props.onQueueRowModalOpenOrClose(true);
this.setState({ isInteractiveImportModalOpen: true });
};
onInteractiveImportModalClose = () => {
this.props.onQueueRowModalOpenOrClose(false);
this.setState({ isInteractiveImportModalOpen: false });
};
//
// Render
render() {
const {
id,
downloadId,
title,
status,
trackedDownloadStatus,
trackedDownloadState,
statusMessages,
errorMessage,
movie,
quality,
customFormats,
customFormatScore,
languages,
protocol,
indexer,
outputPath,
downloadClient,
estimatedCompletionTime,
timeleft,
size,
sizeleft,
showRelativeDates,
shortDateFormat,
timeFormat,
isGrabbing,
grabError,
isRemoving,
isSelected,
columns,
onSelectedChange,
onGrabPress
} = this.props;
const {
isRemoveQueueItemModalOpen,
isInteractiveImportModalOpen
} = this.state;
const progress = 100 - (sizeleft / size * 100);
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'status') {
return (
<QueueStatusCell
key={name}
sourceTitle={title}
status={status}
trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages}
errorMessage={errorMessage}
/>
);
}
if (name === 'movies.sortTitle') {
return (
<TableRowCell key={name}>
{
movie ?
<MovieTitleLink
titleSlug={movie.titleSlug}
title={movie.title}
/> :
title
}
</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguage
languages={languages}
/>
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
{
quality ?
<MovieQuality
quality={quality}
/> :
null
}
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats
formats={customFormats}
/>
</TableRowCell>
);
}
if (name === 'customFormatScore') {
return (
<TableRowCell
key={name}
className={styles.customFormatScore}
>
<Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
/>
</TableRowCell>
);
}
if (name === 'protocol') {
return (
<TableRowCell key={name}>
<ProtocolLabel
protocol={protocol}
/>
</TableRowCell>
);
}
if (name === 'indexer') {
return (
<TableRowCell key={name}>
{indexer}
</TableRowCell>
);
}
if (name === 'downloadClient') {
return (
<TableRowCell key={name}>
{downloadClient}
</TableRowCell>
);
}
if (name === 'size') {
return (
<TableRowCell key={name}>
{formatBytes(size)}
</TableRowCell>
);
}
if (name === 'year') {
return (
<TableRowCell key={name}>
{
movie ? movie.year : ''
}
</TableRowCell>
);
}
if (name === 'title') {
return (
<TableRowCell key={name}>
{title}
</TableRowCell>
);
}
if (name === 'outputPath') {
return (
<TableRowCell key={name}>
{outputPath}
</TableRowCell>
);
}
if (name === 'estimatedCompletionTime') {
return (
<TimeleftCell
key={name}
status={status}
estimatedCompletionTime={estimatedCompletionTime}
timeleft={timeleft}
size={size}
sizeleft={sizeleft}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
/>
);
}
if (name === 'progress') {
return (
<TableRowCell
key={name}
className={styles.progress}
>
{
!!progress &&
<ProgressBar
progress={progress}
title={`${progress.toFixed(1)}%`}
/>
}
</TableRowCell>
);
}
if (name === 'actions') {
return (
<TableRowCell
key={name}
className={styles.actions}
>
{
showInteractiveImport &&
<IconButton
name={icons.INTERACTIVE}
onPress={this.onInteractiveImportPress}
/>
}
{
isPending &&
<SpinnerIconButton
name={icons.DOWNLOAD}
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
isSpinning={isGrabbing}
onPress={onGrabPress}
/>
}
<SpinnerIconButton
title={translate('RemoveFromQueue')}
name={icons.REMOVE}
isSpinning={isRemoving}
onPress={this.onRemoveQueueItemPress}
/>
</TableRowCell>
);
}
return null;
})
}
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
downloadId={downloadId}
title={title}
onModalClose={this.onInteractiveImportModalClose}
/>
<RemoveQueueItemModal
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canIgnore={!!movie}
isPending={isPending}
onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose}
/>
</TableRow>
);
}
}
QueueRow.propTypes = {
id: PropTypes.number.isRequired,
downloadId: PropTypes.string,
title: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string,
trackedDownloadState: PropTypes.string,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string,
movie: PropTypes.object,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
outputPath: PropTypes.string,
downloadClient: PropTypes.string,
estimatedCompletionTime: PropTypes.string,
timeleft: PropTypes.string,
size: PropTypes.number,
year: PropTypes.number,
sizeleft: PropTypes.number,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
isGrabbing: PropTypes.bool.isRequired,
grabError: PropTypes.object,
isRemoving: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired,
onRemoveQueueItemPress: PropTypes.func.isRequired,
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
};
QueueRow.defaultProps = {
customFormats: [],
isGrabbing: false,
isRemoving: false
};
export default QueueRow;

View File

@@ -1,361 +0,0 @@
import React, { useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import { Error } from 'App/State/AppSectionState';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Column from 'Components/Table/Column';
import TableRow from 'Components/Table/TableRow';
import Tooltip from 'Components/Tooltip/Tooltip';
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import Language from 'Language/Language';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguages from 'Movie/MovieLanguages';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import useMovie from 'Movie/useMovie';
import { QualityModel } from 'Quality/Quality';
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import CustomFormat from 'typings/CustomFormat';
import { SelectStateInputProps } from 'typings/props';
import {
QueueTrackedDownloadState,
QueueTrackedDownloadStatus,
StatusMessage,
} from 'typings/Queue';
import formatBytes from 'Utilities/Number/formatBytes';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import translate from 'Utilities/String/translate';
import QueueStatusCell from './QueueStatusCell';
import RemoveQueueItemModal, { RemovePressProps } from './RemoveQueueItemModal';
import TimeleftCell from './TimeleftCell';
import styles from './QueueRow.css';
interface QueueRowProps {
id: number;
movieId?: number;
downloadId?: string;
title: string;
status: string;
trackedDownloadStatus?: QueueTrackedDownloadStatus;
trackedDownloadState?: QueueTrackedDownloadState;
statusMessages?: StatusMessage[];
errorMessage?: string;
languages: Language[];
quality: QualityModel;
customFormats?: CustomFormat[];
customFormatScore: number;
protocol: DownloadProtocol;
indexer?: string;
outputPath?: string;
downloadClient?: string;
downloadClientHasPostImportCategory?: boolean;
estimatedCompletionTime?: string;
added?: string;
timeleft?: string;
size: number;
sizeleft: number;
isGrabbing?: boolean;
grabError?: Error;
isRemoving?: boolean;
isSelected?: boolean;
columns: Column[];
onSelectedChange: (options: SelectStateInputProps) => void;
onQueueRowModalOpenOrClose: (isOpen: boolean) => void;
}
function QueueRow(props: QueueRowProps) {
const {
id,
movieId,
downloadId,
title,
status,
trackedDownloadStatus,
trackedDownloadState,
statusMessages,
errorMessage,
languages,
quality,
customFormats = [],
customFormatScore,
protocol,
indexer,
outputPath,
downloadClient,
downloadClientHasPostImportCategory,
estimatedCompletionTime,
added,
timeleft,
size,
sizeleft,
isGrabbing = false,
grabError,
isRemoving = false,
isSelected,
columns,
onSelectedChange,
onQueueRowModalOpenOrClose,
} = props;
const dispatch = useDispatch();
const movie = useMovie(movieId);
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
createUISettingsSelector()
);
const [isRemoveQueueItemModalOpen, setIsRemoveQueueItemModalOpen] =
useState(false);
const [isInteractiveImportModalOpen, setIsInteractiveImportModalOpen] =
useState(false);
const handleGrabPress = useCallback(() => {
dispatch(grabQueueItem({ id }));
}, [id, dispatch]);
const handleInteractiveImportPress = useCallback(() => {
onQueueRowModalOpenOrClose(true);
setIsInteractiveImportModalOpen(true);
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
const handleInteractiveImportModalClose = useCallback(() => {
onQueueRowModalOpenOrClose(false);
setIsInteractiveImportModalOpen(false);
}, [setIsInteractiveImportModalOpen, onQueueRowModalOpenOrClose]);
const handleRemoveQueueItemPress = useCallback(() => {
onQueueRowModalOpenOrClose(true);
setIsRemoveQueueItemModalOpen(true);
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
const handleRemoveQueueItemModalConfirmed = useCallback(
(payload: RemovePressProps) => {
onQueueRowModalOpenOrClose(false);
dispatch(removeQueueItem({ id, ...payload }));
setIsRemoveQueueItemModalOpen(false);
},
[id, setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose, dispatch]
);
const handleRemoveQueueItemModalClose = useCallback(() => {
onQueueRowModalOpenOrClose(false);
setIsRemoveQueueItemModalOpen(false);
}, [setIsRemoveQueueItemModalOpen, onQueueRowModalOpenOrClose]);
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
const showInteractiveImport =
status === 'completed' && trackedDownloadStatus === 'warning';
const isPending =
status === 'delay' || status === 'downloadClientUnavailable';
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{columns.map((column) => {
const { name, isVisible } = column;
if (!isVisible) {
return null;
}
if (name === 'status') {
return (
<QueueStatusCell
key={name}
sourceTitle={title}
status={status}
trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages}
errorMessage={errorMessage}
/>
);
}
if (name === 'movies.sortTitle') {
return (
<TableRowCell key={name}>
{movie ? (
<MovieTitleLink
titleSlug={movie.titleSlug}
title={movie.title}
/>
) : (
title
)}
</TableRowCell>
);
}
if (name === 'year') {
return (
<TableRowCell key={name}>{movie ? movie.year : ''}</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguages languages={languages} />
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
{quality ? <MovieQuality quality={quality} /> : null}
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats formats={customFormats} />
</TableRowCell>
);
}
if (name === 'customFormatScore') {
return (
<TableRowCell key={name} className={styles.customFormatScore}>
<Tooltip
anchor={formatCustomFormatScore(
customFormatScore,
customFormats.length
)}
tooltip={<MovieFormats formats={customFormats} />}
position={tooltipPositions.BOTTOM}
/>
</TableRowCell>
);
}
if (name === 'protocol') {
return (
<TableRowCell key={name}>
<ProtocolLabel protocol={protocol} />
</TableRowCell>
);
}
if (name === 'indexer') {
return <TableRowCell key={name}>{indexer}</TableRowCell>;
}
if (name === 'downloadClient') {
return <TableRowCell key={name}>{downloadClient}</TableRowCell>;
}
if (name === 'title') {
return <TableRowCell key={name}>{title}</TableRowCell>;
}
if (name === 'size') {
return <TableRowCell key={name}>{formatBytes(size)}</TableRowCell>;
}
if (name === 'outputPath') {
return <TableRowCell key={name}>{outputPath}</TableRowCell>;
}
if (name === 'estimatedCompletionTime') {
return (
<TimeleftCell
key={name}
status={status}
estimatedCompletionTime={estimatedCompletionTime}
timeleft={timeleft}
size={size}
sizeleft={sizeleft}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
/>
);
}
if (name === 'progress') {
return (
<TableRowCell key={name} className={styles.progress}>
{!!progress && (
<ProgressBar
progress={progress}
title={`${progress.toFixed(1)}%`}
/>
)}
</TableRowCell>
);
}
if (name === 'added') {
return <RelativeDateCell key={name} date={added} />;
}
if (name === 'actions') {
return (
<TableRowCell key={name} className={styles.actions}>
{showInteractiveImport ? (
<IconButton
name={icons.INTERACTIVE}
onPress={handleInteractiveImportPress}
/>
) : null}
{isPending ? (
<SpinnerIconButton
name={icons.DOWNLOAD}
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
isSpinning={isGrabbing}
onPress={handleGrabPress}
/>
) : null}
<SpinnerIconButton
title={translate('RemoveFromQueue')}
name={icons.REMOVE}
isSpinning={isRemoving}
onPress={handleRemoveQueueItemPress}
/>
</TableRowCell>
);
}
return null;
})}
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
downloadId={downloadId}
modalTitle={title}
onModalClose={handleInteractiveImportModalClose}
/>
<RemoveQueueItemModal
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canChangeCategory={!!downloadClientHasPostImportCategory}
canIgnore={!!movie}
isPending={isPending}
onRemovePress={handleRemoveQueueItemModalConfirmed}
onModalClose={handleRemoveQueueItemModalClose}
/>
</TableRow>
);
}
export default QueueRow;

View File

@@ -0,0 +1,67 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import QueueRow from './QueueRow';
function createMapStateToProps() {
return createSelector(
createMovieSelector(),
createUISettingsSelector(),
(movie, uiSettings) => {
const result = {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};
result.movie = movie;
return result;
}
);
}
const mapDispatchToProps = {
grabQueueItem,
removeQueueItem
};
class QueueRowConnector extends Component {
//
// Listeners
onGrabPress = () => {
this.props.grabQueueItem({ id: this.props.id });
};
onRemoveQueueItemPress = (payload) => {
this.props.removeQueueItem({ id: this.props.id, ...payload });
};
//
// Render
render() {
return (
<QueueRow
{...this.props}
onGrabPress={this.onGrabPress}
onRemoveQueueItemPress={this.onRemoveQueueItemPress}
/>
);
}
}
QueueRowConnector.propTypes = {
id: PropTypes.number.isRequired,
movie: PropTypes.object,
grabQueueItem: PropTypes.func.isRequired,
removeQueueItem: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector);

View File

@@ -1,59 +1,51 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon, { IconProps } from 'Components/Icon';
import Icon from 'Components/Icon';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds } from 'Helpers/Props';
import TooltipPosition from 'Helpers/Props/TooltipPosition';
import {
QueueTrackedDownloadState,
QueueTrackedDownloadStatus,
StatusMessage,
} from 'typings/Queue';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './QueueStatus.css';
function getDetailedPopoverBody(statusMessages: StatusMessage[]) {
function getDetailedPopoverBody(statusMessages) {
return (
<div>
{statusMessages.map(({ title, messages }) => {
return (
<div
key={title}
className={messages.length ? undefined : styles.noMessages}
>
{title}
<ul>
{messages.map((message) => {
return <li key={message}>{message}</li>;
})}
</ul>
</div>
);
})}
{
statusMessages.map(({ title, messages }) => {
return (
<div
key={title}
className={messages.length ? undefined: styles.noMessages}
>
{title}
<ul>
{
messages.map((message) => {
return (
<li key={message}>
{message}
</li>
);
})
}
</ul>
</div>
);
})
}
</div>
);
}
interface QueueStatusProps {
sourceTitle: string;
status: string;
trackedDownloadStatus?: QueueTrackedDownloadStatus;
trackedDownloadState?: QueueTrackedDownloadState;
statusMessages?: StatusMessage[];
errorMessage?: string;
position: TooltipPosition;
canFlip?: boolean;
}
function QueueStatus(props: QueueStatusProps) {
function QueueStatus(props) {
const {
sourceTitle,
status,
trackedDownloadStatus = 'ok',
trackedDownloadState = 'downloading',
statusMessages = [],
trackedDownloadStatus,
trackedDownloadState,
statusMessages,
errorMessage,
position,
canFlip = false,
canFlip
} = props;
const hasWarning = trackedDownloadStatus === 'warning';
@@ -61,7 +53,7 @@ function QueueStatus(props: QueueStatusProps) {
// status === 'downloading'
let iconName = icons.DOWNLOADING;
let iconKind: IconProps['kind'] = kinds.DEFAULT;
let iconKind = kinds.DEFAULT;
let title = translate('Downloading');
if (status === 'paused') {
@@ -78,11 +70,6 @@ function QueueStatus(props: QueueStatusProps) {
iconName = icons.DOWNLOADED;
title = translate('Downloaded');
if (trackedDownloadState === 'importBlocked') {
title += ` - ${translate('UnableToImportAutomatically')}`;
iconKind = kinds.WARNING;
}
if (trackedDownloadState === 'importPending') {
title += ` - ${translate('WaitingToImport')}`;
iconKind = kinds.PURPLE;
@@ -123,8 +110,7 @@ function QueueStatus(props: QueueStatusProps) {
if (status === 'warning') {
iconName = icons.DOWNLOADING;
iconKind = kinds.WARNING;
const warningMessage =
errorMessage || translate('CheckDownloadClientForDetails');
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
title = translate('DownloadWarning', { warningMessage });
}
@@ -142,17 +128,35 @@ function QueueStatus(props: QueueStatusProps) {
return (
<Popover
anchor={<Icon name={iconName} kind={iconKind} />}
title={title}
body={
hasWarning || hasError
? getDetailedPopoverBody(statusMessages)
: sourceTitle
anchor={
<Icon
name={iconName}
kind={iconKind}
/>
}
title={title}
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
position={position}
canFlip={canFlip}
/>
);
}
QueueStatus.propTypes = {
sourceTitle: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string,
position: PropTypes.oneOf(tooltipPositions.all).isRequired,
canFlip: PropTypes.bool.isRequired
};
QueueStatus.defaultProps = {
trackedDownloadStatus: 'ok',
trackedDownloadState: 'downloading',
canFlip: false
};
export default QueueStatus;

View File

@@ -0,0 +1,48 @@
import PropTypes from 'prop-types';
import React from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import { tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import QueueStatus from './QueueStatus';
import styles from './QueueStatusCell.css';
function QueueStatusCell(props) {
const {
sourceTitle,
status,
trackedDownloadStatus,
trackedDownloadState,
statusMessages,
errorMessage
} = props;
return (
<TableRowCell className={styles.status}>
<QueueStatus
sourceTitle={sourceTitle}
status={status}
trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages}
errorMessage={errorMessage}
position={tooltipPositions.RIGHT}
/>
</TableRowCell>
);
}
QueueStatusCell.propTypes = {
sourceTitle: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string
};
QueueStatusCell.defaultProps = {
trackedDownloadStatus: translate('Ok'),
trackedDownloadState: translate('Downloading')
};
export default QueueStatusCell;

View File

@@ -1,45 +0,0 @@
import React from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import {
QueueTrackedDownloadState,
QueueTrackedDownloadStatus,
StatusMessage,
} from 'typings/Queue';
import QueueStatus from './QueueStatus';
import styles from './QueueStatusCell.css';
interface QueueStatusCellProps {
sourceTitle: string;
status: string;
trackedDownloadStatus?: QueueTrackedDownloadStatus;
trackedDownloadState?: QueueTrackedDownloadState;
statusMessages?: StatusMessage[];
errorMessage?: string;
}
function QueueStatusCell(props: QueueStatusCellProps) {
const {
sourceTitle,
status,
trackedDownloadStatus = 'ok',
trackedDownloadState = 'downloading',
statusMessages,
errorMessage,
} = props;
return (
<TableRowCell className={styles.status}>
<QueueStatus
sourceTitle={sourceTitle}
status={status}
trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages}
errorMessage={errorMessage}
position="right"
/>
</TableRowCell>
);
}
export default QueueStatusCell;

View File

@@ -0,0 +1,171 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class RemoveQueueItemModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
remove: true,
blocklist: false,
skipRedownload: false
};
}
//
// Control
resetState = function() {
this.setState({
remove: true,
blocklist: false,
skipRedownload: false
});
};
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
};
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
};
onSkipRedownloadChange = ({ value }) => {
this.setState({ skipRedownload: value });
};
onRemoveConfirmed = () => {
const state = this.state;
this.resetState();
this.props.onRemovePress(state);
};
onModalClose = () => {
this.resetState();
this.props.onModalClose();
};
//
// Render
render() {
const {
isOpen,
sourceTitle,
canIgnore,
isPending
} = this.props;
const { remove, blocklist, skipRedownload } = this.state;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={this.onModalClose}
>
<ModalContent
onModalClose={this.onModalClose}
>
<ModalHeader>
{translate('RemoveQueueItem', { sourceTitle })}
</ModalHeader>
<ModalBody>
<div>
{translate('RemoveQueueItemConfirmation', { sourceTitle })}
</div>
{
isPending ?
null :
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistReleaseHelpText')}
onChange={this.onBlocklistChange}
/>
</FormGroup>
{
blocklist ?
<FormGroup>
<FormLabel>{translate('SkipRedownload')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipRedownload"
value={skipRedownload}
helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipRedownloadChange}
/>
</FormGroup> :
null
}
</ModalBody>
<ModalFooter>
<Button onPress={this.onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onRemoveConfirmed}
>
{translate('Remove')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
RemoveQueueItemModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
canIgnore: PropTypes.bool.isRequired,
isPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default RemoveQueueItemModal;

View File

@@ -1,231 +0,0 @@
import React, { useCallback, useMemo, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RemoveQueueItemModal.css';
export interface RemovePressProps {
remove: boolean;
changeCategory: boolean;
blocklist: boolean;
skipRedownload: boolean;
}
interface RemoveQueueItemModalProps {
isOpen: boolean;
sourceTitle?: string;
canChangeCategory: boolean;
canIgnore: boolean;
isPending: boolean;
selectedCount?: number;
onRemovePress(props: RemovePressProps): void;
onModalClose: () => void;
}
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
type BlocklistMethod =
| 'doNotBlocklist'
| 'blocklistAndSearch'
| 'blocklistOnly';
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
const {
isOpen,
sourceTitle = '',
canIgnore,
canChangeCategory,
isPending,
selectedCount,
onRemovePress,
onModalClose,
} = props;
const multipleSelected = selectedCount && selectedCount > 1;
const [removalMethod, setRemovalMethod] =
useState<RemovalMethod>('removeFromClient');
const [blocklistMethod, setBlocklistMethod] =
useState<BlocklistMethod>('doNotBlocklist');
const { title, message } = useMemo(() => {
if (!selectedCount) {
return {
title: translate('RemoveQueueItem', { sourceTitle }),
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
};
}
if (selectedCount === 1) {
return {
title: translate('RemoveSelectedItem'),
message: translate('RemoveSelectedItemQueueMessageText'),
};
}
return {
title: translate('RemoveSelectedItems'),
message: translate('RemoveSelectedItemsQueueMessageText', {
selectedCount,
}),
};
}, [sourceTitle, selectedCount]);
const removalMethodOptions = useMemo(() => {
return [
{
key: 'removeFromClient',
value: translate('RemoveFromDownloadClient'),
hint: multipleSelected
? translate('RemoveMultipleFromDownloadClientHint')
: translate('RemoveFromDownloadClientHint'),
},
{
key: 'changeCategory',
value: translate('ChangeCategory'),
isDisabled: !canChangeCategory,
hint: multipleSelected
? translate('ChangeCategoryMultipleHint')
: translate('ChangeCategoryHint'),
},
{
key: 'ignore',
value: multipleSelected
? translate('IgnoreDownloads')
: translate('IgnoreDownload'),
isDisabled: !canIgnore,
hint: multipleSelected
? translate('IgnoreDownloadsHint')
: translate('IgnoreDownloadHint'),
},
];
}, [canChangeCategory, canIgnore, multipleSelected]);
const blocklistMethodOptions = useMemo(() => {
return [
{
key: 'doNotBlocklist',
value: translate('DoNotBlocklist'),
hint: translate('DoNotBlocklistHint'),
},
{
key: 'blocklistAndSearch',
value: translate('BlocklistAndSearch'),
isDisabled: isPending,
hint: multipleSelected
? translate('BlocklistAndSearchMultipleHint')
: translate('BlocklistAndSearchHint'),
},
{
key: 'blocklistOnly',
value: translate('BlocklistOnly'),
hint: multipleSelected
? translate('BlocklistMultipleOnlyHint')
: translate('BlocklistOnlyHint'),
},
];
}, [isPending, multipleSelected]);
const handleRemovalMethodChange = useCallback(
({ value }: { value: RemovalMethod }) => {
setRemovalMethod(value);
},
[setRemovalMethod]
);
const handleBlocklistMethodChange = useCallback(
({ value }: { value: BlocklistMethod }) => {
setBlocklistMethod(value);
},
[setBlocklistMethod]
);
const handleConfirmRemove = useCallback(() => {
onRemovePress({
remove: removalMethod === 'removeFromClient',
changeCategory: removalMethod === 'changeCategory',
blocklist: blocklistMethod !== 'doNotBlocklist',
skipRedownload: blocklistMethod === 'blocklistOnly',
});
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
}, [
removalMethod,
blocklistMethod,
setRemovalMethod,
setBlocklistMethod,
onRemovePress,
]);
const handleModalClose = useCallback(() => {
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
onModalClose();
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
<ModalContent onModalClose={handleModalClose}>
<ModalHeader>{title}</ModalHeader>
<ModalBody>
<div className={styles.message}>{message}</div>
{isPending ? null : (
<FormGroup>
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="removalMethod"
value={removalMethod}
values={removalMethodOptions}
isDisabled={!canChangeCategory && !canIgnore}
helpTextWarning={translate(
'RemoveQueueItemRemovalMethodHelpTextWarning'
)}
onChange={handleRemovalMethodChange}
/>
</FormGroup>
)}
<FormGroup>
<FormLabel>
{multipleSelected
? translate('BlocklistReleases')
: translate('BlocklistRelease')}
</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="blocklistMethod"
value={blocklistMethod}
values={blocklistMethodOptions}
helpText={translate('BlocklistReleaseHelpText')}
onChange={handleBlocklistMethodChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button onPress={handleModalClose}>{translate('Close')}</Button>
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
{translate('Remove')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
export default RemoveQueueItemModal;

View File

@@ -0,0 +1,174 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RemoveQueueItemsModal.css';
class RemoveQueueItemsModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
remove: true,
blocklist: false,
skipRedownload: false
};
}
//
// Control
resetState = function() {
this.setState({
remove: true,
blocklist: false,
skipRedownload: false
});
};
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
};
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
};
onSkipRedownloadChange = ({ value }) => {
this.setState({ skipRedownload: value });
};
onRemoveConfirmed = () => {
const state = this.state;
this.resetState();
this.props.onRemovePress(state);
};
onModalClose = () => {
this.resetState();
this.props.onModalClose();
};
//
// Render
render() {
const {
isOpen,
selectedCount,
canIgnore,
allPending
} = this.props;
const { remove, blocklist, skipRedownload } = this.state;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={this.onModalClose}
>
<ModalContent
onModalClose={this.onModalClose}
>
<ModalHeader>
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
</ModalHeader>
<ModalBody>
<div className={styles.message}>
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
</div>
{
allPending ?
null :
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistReleaseHelpText')}
onChange={this.onBlocklistChange}
/>
</FormGroup>
{
blocklist ?
<FormGroup>
<FormLabel>{translate('SkipRedownload')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipRedownload"
value={skipRedownload}
helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipRedownloadChange}
/>
</FormGroup> :
null
}
</ModalBody>
<ModalFooter>
<Button onPress={this.onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onRemoveConfirmed}
>
{translate('Remove')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
RemoveQueueItemsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
selectedCount: PropTypes.number.isRequired,
canIgnore: PropTypes.bool.isRequired,
allPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default RemoveQueueItemsModal;

View File

@@ -1,37 +0,0 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { fetchQueueStatus } from 'Store/Actions/queueActions';
import createQueueStatusSelector from './createQueueStatusSelector';
function QueueStatus() {
const dispatch = useDispatch();
const { isConnected, isReconnecting } = useSelector(
(state: AppState) => state.app
);
const { isPopulated, count, errors, warnings } = useSelector(
createQueueStatusSelector()
);
const wasReconnecting = usePrevious(isReconnecting);
useEffect(() => {
if (!isPopulated) {
dispatch(fetchQueueStatus());
}
}, [isPopulated, dispatch]);
useEffect(() => {
if (isConnected && wasReconnecting) {
dispatch(fetchQueueStatus());
}
}, [isConnected, wasReconnecting, dispatch]);
return (
<PageSidebarStatus count={count} errors={errors} warnings={warnings} />
);
}
export default QueueStatus;

View File

@@ -0,0 +1,76 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
import { fetchQueueStatus } from 'Store/Actions/queueActions';
function createMapStateToProps() {
return createSelector(
(state) => state.app,
(state) => state.queue.status,
(state) => state.queue.options.includeUnknownMovieItems,
(app, status, includeUnknownMovieItems) => {
const {
errors,
warnings,
unknownErrors,
unknownWarnings,
count,
totalCount
} = status.item;
return {
isConnected: app.isConnected,
isReconnecting: app.isReconnecting,
isPopulated: status.isPopulated,
...status.item,
count: includeUnknownMovieItems ? totalCount : count,
errors: includeUnknownMovieItems ? errors || unknownErrors : errors,
warnings: includeUnknownMovieItems ? warnings || unknownWarnings : warnings
};
}
);
}
const mapDispatchToProps = {
fetchQueueStatus
};
class QueueStatusConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isPopulated) {
this.props.fetchQueueStatus();
}
}
componentDidUpdate(prevProps) {
if (this.props.isConnected && prevProps.isReconnecting) {
this.props.fetchQueueStatus();
}
}
//
// Render
render() {
return (
<PageSidebarStatus
{...this.props}
/>
);
}
}
QueueStatusConnector.propTypes = {
isConnected: PropTypes.bool.isRequired,
isReconnecting: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
fetchQueueStatus: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector);

View File

@@ -1,32 +0,0 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createQueueStatusSelector() {
return createSelector(
(state: AppState) => state.queue.status.isPopulated,
(state: AppState) => state.queue.status.item,
(state: AppState) => state.queue.options.includeUnknownMovieItems,
(isPopulated, status, includeUnknownMovieItems) => {
const {
errors,
warnings,
unknownErrors,
unknownWarnings,
count,
totalCount,
} = status;
return {
...status,
isPopulated,
count: includeUnknownMovieItems ? totalCount : count,
errors: includeUnknownMovieItems ? errors || unknownErrors : errors,
warnings: includeUnknownMovieItems
? warnings || unknownWarnings
: warnings,
};
}
);
}
export default createQueueStatusSelector;

View File

@@ -0,0 +1,83 @@
import PropTypes from 'prop-types';
import React from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import formatTime from 'Utilities/Date/formatTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './TimeleftCell.css';
function TimeleftCell(props) {
const {
estimatedCompletionTime,
timeleft,
status,
size,
sizeleft,
showRelativeDates,
shortDateFormat,
timeFormat
} = props;
if (status === 'delay') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (
<TableRowCell
className={styles.timeleft}
title={translate('DelayingDownloadUntil', { date, time })}
>
-
</TableRowCell>
);
}
if (status === 'downloadClientUnavailable') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (
<TableRowCell
className={styles.timeleft}
title={translate('RetryingDownloadOn', { date, time })}
>
-
</TableRowCell>
);
}
if (!timeleft || status === 'completed' || status === 'failed') {
return (
<TableRowCell className={styles.timeleft}>
-
</TableRowCell>
);
}
const totalSize = formatBytes(size);
const remainingSize = formatBytes(sizeleft);
return (
<TableRowCell
className={styles.timeleft}
title={`${remainingSize} / ${totalSize}`}
>
{formatTimeSpan(timeleft)}
</TableRowCell>
);
}
TimeleftCell.propTypes = {
estimatedCompletionTime: PropTypes.string,
timeleft: PropTypes.string,
status: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
sizeleft: PropTypes.number.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
};
export default TimeleftCell;

View File

@@ -1,97 +0,0 @@
import React from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import formatTime from 'Utilities/Date/formatTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './TimeleftCell.css';
interface TimeleftCellProps {
estimatedCompletionTime?: string;
timeleft?: string;
status: string;
size: number;
sizeleft: number;
showRelativeDates: boolean;
shortDateFormat: string;
timeFormat: string;
}
function TimeleftCell(props: TimeleftCellProps) {
const {
estimatedCompletionTime,
timeleft,
status,
size,
sizeleft,
showRelativeDates,
shortDateFormat,
timeFormat,
} = props;
if (status === 'delay') {
const date = getRelativeDate({
date: estimatedCompletionTime,
shortDateFormat,
showRelativeDates,
});
const time = formatTime(estimatedCompletionTime, timeFormat, {
includeMinuteZero: true,
});
return (
<TableRowCell className={styles.timeleft}>
<Tooltip
anchor={<Icon name={icons.INFO} />}
tooltip={translate('DelayingDownloadUntil', { date, time })}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
</TableRowCell>
);
}
if (status === 'downloadClientUnavailable') {
const date = getRelativeDate({
date: estimatedCompletionTime,
shortDateFormat,
showRelativeDates,
});
const time = formatTime(estimatedCompletionTime, timeFormat, {
includeMinuteZero: true,
});
return (
<TableRowCell className={styles.timeleft}>
<Tooltip
anchor={<Icon name={icons.INFO} />}
tooltip={translate('RetryingDownloadOn', { date, time })}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
</TableRowCell>
);
}
if (!timeleft || status === 'completed' || status === 'failed') {
return <TableRowCell className={styles.timeleft}>-</TableRowCell>;
}
const totalSize = formatBytes(size);
const remainingSize = formatBytes(sizeleft);
return (
<TableRowCell
className={styles.timeleft}
title={`${remainingSize} / ${totalSize}`}
>
{formatTimeSpan(timeleft)}
</TableRowCell>
);
}
export default TimeleftCell;

View File

@@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
@@ -131,12 +130,7 @@ class AddNewMovie extends Component {
<div className={styles.helpText}>
{translate('FailedLoadingSearchResults')}
</div>
<Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert>
<div>
<Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb">
{translate('WhySearchesCouldBeFailing')}
</Link>
</div>
<div>{getErrorMessage(error)}</div>
</div> : null
}

View File

@@ -3,12 +3,10 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import parseUrl from 'Utilities/String/parseUrl';
import AddNewMovie from './AddNewMovie';
@@ -35,10 +33,9 @@ const mapDispatchToProps = {
lookupMovie,
clearAddMovie,
fetchRootFolders,
fetchImportExclusions,
fetchQueueDetails,
clearQueueDetails,
fetchMovieFiles,
clearMovieFiles
clearQueueDetails
};
class AddNewMovieConnector extends Component {
@@ -54,23 +51,10 @@ class AddNewMovieConnector extends Component {
componentDidMount() {
this.props.fetchRootFolders();
this.props.fetchImportExclusions();
this.props.fetchQueueDetails();
}
componentDidUpdate(prevProps) {
const {
items
} = this.props;
if (hasDifferentItems(prevProps.items, items)) {
const movieIds = selectUniqueIds(items, 'internalId');
if (movieIds.length) {
this.props.fetchMovieFiles({ movieId: movieIds });
}
}
}
componentWillUnmount() {
if (this._movieLookupTimeout) {
clearTimeout(this._movieLookupTimeout);
@@ -78,7 +62,6 @@ class AddNewMovieConnector extends Component {
this.props.clearAddMovie();
this.props.clearQueueDetails();
this.props.clearMovieFiles();
}
//
@@ -124,14 +107,12 @@ class AddNewMovieConnector extends Component {
AddNewMovieConnector.propTypes = {
term: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
lookupMovie: PropTypes.func.isRequired,
clearAddMovie: PropTypes.func.isRequired,
fetchRootFolders: PropTypes.func.isRequired,
fetchImportExclusions: PropTypes.func.isRequired,
fetchQueueDetails: PropTypes.func.isRequired,
clearQueueDetails: PropTypes.func.isRequired,
fetchMovieFiles: PropTypes.func.isRequired,
clearMovieFiles: PropTypes.func.isRequired
clearQueueDetails: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieConnector);

View File

@@ -85,14 +85,8 @@
margin-top: 20px;
}
.originalLanguage,
.studio,
.genres {
margin-left: 5px;
}
.links {
margin-left: 5px;
margin-left: 8px;
pointer-events: all;
}

View File

@@ -5,10 +5,8 @@ interface CssExports {
'certification': string;
'content': string;
'exclusionIcon': string;
'genres': string;
'icons': string;
'links': string;
'originalLanguage': string;
'overlay': string;
'overview': string;
'poster': string;
@@ -16,7 +14,6 @@ interface CssExports {
'runtime': string;
'searchResult': string;
'statusContainer': string;
'studio': string;
'title': string;
'titleContainer': string;
'titleRow': string;

View File

@@ -1,7 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import TmdbRating from 'Components/TmdbRating';
@@ -62,23 +61,21 @@ class AddNewMovieSearchResult extends Component {
titleSlug,
year,
studio,
originalLanguage,
genres,
status,
overview,
ratings,
folder,
images,
existingMovieId,
isExistingMovie,
isExcluded,
isExclusionMovie,
isSmallScreen,
colorImpairedMode,
id,
monitored,
hasFile,
isAvailable,
movieFile,
queueItem,
queueStatus,
queueState,
runtime,
movieRuntimeFormat,
certification
@@ -88,8 +85,6 @@ class AddNewMovieSearchResult extends Component {
isNewAddMovieModalOpen
} = this.state;
const hasMovieFile = !!movieFile;
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
const posterWidth = 167;
const posterHeight = 250;
@@ -125,13 +120,13 @@ class AddNewMovieSearchResult extends Component {
{
isExistingMovie &&
<MovieIndexProgressBar
movieId={existingMovieId}
movieFile={movieFile}
monitored={monitored}
hasFile={hasMovieFile}
hasFile={hasFile}
status={status}
width={posterWidth}
detailedProgressBar={true}
queueStatus={queueStatus}
queueState={queueState}
isAvailable={isAvailable}
/>
}
@@ -155,27 +150,26 @@ class AddNewMovieSearchResult extends Component {
</div>
<div className={styles.icons}>
<div>
{
isExistingMovie &&
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title={translate('AlreadyInYourLibrary')}
/>
}
{
isExcluded &&
<Icon
className={styles.exclusionIcon}
name={icons.DANGER}
size={36}
title={translate('MovieIsOnImportExclusionList')}
/>
}
</div>
{
isExistingMovie &&
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title={translate('AlreadyInYourLibrary')}
/>
}
{
isExclusionMovie &&
<Icon
className={styles.exclusionIcon}
name={icons.DANGER}
size={36}
title={translate('MovieIsOnImportExclusionList')}
/>
}
</div>
</div>
@@ -204,56 +198,10 @@ class AddNewMovieSearchResult extends Component {
</Label>
{
ratings.imdb ?
!!studio &&
<Label size={sizes.LARGE}>
<ImdbRating
ratings={ratings}
iconSize={13}
/>
</Label> :
null
}
{
originalLanguage?.name ?
<Label size={sizes.LARGE}>
<Icon
name={icons.LANGUAGE}
size={13}
/>
<span className={styles.originalLanguage}>
{originalLanguage.name}
</span>
</Label> :
null
}
{
studio ?
<Label size={sizes.LARGE}>
<Icon
name={icons.STUDIO}
size={13}
/>
<span className={styles.studio}>
{studio}
</span>
</Label> :
null
}
{
genres.length > 0 ?
<Label size={sizes.LARGE}>
<Icon
name={icons.GENRE}
size={13}
/>
<span className={styles.genres}>
{genres.slice(0, 3).join(', ')}
</span>
</Label> :
null
{studio}
</Label>
}
<Tooltip
@@ -267,15 +215,15 @@ class AddNewMovieSearchResult extends Component {
/>
<span className={styles.links}>
{translate('Links')}
Links
</span>
</Label>
}
tooltip={
<MovieDetailsLinks
tmdbId={tmdbId}
imdbId={imdbId}
youTubeTrailerId={youTubeTrailerId}
imdbId={imdbId}
/>
}
canFlip={true}
@@ -286,11 +234,9 @@ class AddNewMovieSearchResult extends Component {
{
isExistingMovie && isSmallScreen &&
<MovieStatusLabel
status={status}
hasMovieFiles={hasMovieFile}
hasMovieFiles={hasFile}
monitored={monitored}
isAvailable={isAvailable}
queueItem={queueItem}
id={id}
useLabel={true}
colorImpairedMode={colorImpairedMode}
@@ -327,31 +273,25 @@ AddNewMovieSearchResult.propTypes = {
titleSlug: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
studio: PropTypes.string,
originalLanguage: PropTypes.object,
genres: PropTypes.arrayOf(PropTypes.string),
status: PropTypes.string.isRequired,
overview: PropTypes.string,
ratings: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
existingMovieId: PropTypes.number,
isExistingMovie: PropTypes.bool.isRequired,
isExcluded: PropTypes.bool,
isExclusionMovie: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
id: PropTypes.number,
queueItems: PropTypes.arrayOf(PropTypes.object),
monitored: PropTypes.bool.isRequired,
hasFile: PropTypes.bool.isRequired,
isAvailable: PropTypes.bool.isRequired,
movieFile: PropTypes.object,
queueItem: PropTypes.object,
colorImpairedMode: PropTypes.bool,
queueStatus: PropTypes.string,
queueState: PropTypes.string,
runtime: PropTypes.number.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired,
certification: PropTypes.string
};
AddNewMovieSearchResult.defaultProps = {
genres: [],
isExcluded: false
};
export default AddNewMovieSearchResult;

View File

@@ -1,28 +1,26 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createExclusionMovieSelector from 'Store/Selectors/createExclusionMovieSelector';
import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector';
import AddNewMovieSearchResult from './AddNewMovieSearchResult';
function createMapStateToProps() {
return createSelector(
createExistingMovieSelector(),
createExclusionMovieSelector(),
createDimensionsSelector(),
(state) => state.queue.details.items,
(state) => state.movieFiles.items,
(state, { internalId }) => internalId,
(state) => state.settings.ui.item.movieRuntimeFormat,
(isExistingMovie, dimensions, queueItems, movieFiles, internalId, movieRuntimeFormat) => {
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
const movieFile = movieFiles.find((item) => internalId > 0 && item.movieId === internalId);
(isExistingMovie, isExclusionMovie, dimensions, queueItems, internalId) => {
const firstQueueItem = queueItems.find((q) => q.movieId === internalId && internalId > 0);
return {
existingMovieId: internalId,
isExistingMovie,
isExclusionMovie,
isSmallScreen: dimensions.isSmallScreen,
queueItem,
movieFile,
movieRuntimeFormat
queueStatus: firstQueueItem ? firstQueueItem.status : null,
queueState: firstQueueItem ? firstQueueItem.trackedDownloadState : null
};
}
);

View File

@@ -10,7 +10,6 @@ import styles from './ImportMovieRow.css';
function ImportMovieRow(props) {
const {
id,
relativePath,
monitor,
qualityProfileId,
minimumAvailability,
@@ -32,7 +31,7 @@ function ImportMovieRow(props) {
/>
<VirtualTableRowCell className={styles.folder}>
{relativePath}
{id}
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.movie}>
@@ -74,7 +73,6 @@ function ImportMovieRow(props) {
ImportMovieRow.propTypes = {
id: PropTypes.string.isRequired,
relativePath: PropTypes.string.isRequired,
monitor: PropTypes.string.isRequired,
qualityProfileId: PropTypes.number.isRequired,
minimumAvailability: PropTypes.string.isRequired,

View File

@@ -30,7 +30,7 @@ class ImportMovieTable extends Component {
unmappedFolders.forEach((unmappedFolder) => {
const id = unmappedFolder.name;
onMovieLookup(id, unmappedFolder.path, unmappedFolder.relativePath);
onMovieLookup(id, unmappedFolder.path);
onSetImportMovieValue({
id,

View File

@@ -25,11 +25,10 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
onMovieLookup(name, path, relativePath) {
onMovieLookup(name, path) {
dispatch(queueLookupMovie({
name,
path,
relativePath,
term: name
}));
},

View File

@@ -32,7 +32,7 @@
.contentContainer {
z-index: $popperZIndex;
margin-top: 4px;
/* 400px container width with 8px padding on each side */
/* 400px container witdh with 8px padding on each side */
width: 384px;
}

View File

@@ -148,7 +148,7 @@ class ImportMovieSelectFolder extends Component {
className={styles.addErrorAlert}
kind={kinds.DANGER}
>
{translate('AddRootFolderError')}
{translate('UnableToAddRootFolder')}
<ul>
{

View File

@@ -5,13 +5,12 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addRootFolder, deleteRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import ImportMovieSelectFolder from './ImportMovieSelectFolder';
function createMapStateToProps() {
return createSelector(
createRootFoldersSelector(),
(state) => state.rootFolders,
createSystemStatusSelector(),
(rootFolders, systemStatus) => {
return {

View File

@@ -12,10 +12,11 @@ function App({ store, history }) {
<DocumentTitle title={window.Radarr.instanceName}>
<Provider store={store}>
<ConnectedRouter history={history}>
<ApplyTheme />
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
<ApplyTheme>
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ApplyTheme>
</ConnectedRouter>
</Provider>
</DocumentTitle>

View File

@@ -1,9 +1,9 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import Blocklist from 'Activity/Blocklist/Blocklist';
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
import HistoryConnector from 'Activity/History/HistoryConnector';
import Queue from 'Activity/Queue/Queue';
import QueueConnector from 'Activity/Queue/QueueConnector';
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
@@ -33,8 +33,6 @@ import Status from 'System/Status/Status';
import Tasks from 'System/Tasks/Tasks';
import UpdatesConnector from 'System/Updates/UpdatesConnector';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
function AppRoutes(props) {
const {
@@ -115,26 +113,12 @@ function AppRoutes(props) {
<Route
path="/activity/queue"
component={Queue}
component={QueueConnector}
/>
<Route
path="/activity/blocklist"
component={Blocklist}
/>
{/*
Wanted
*/}
<Route
path="/wanted/missing"
component={MissingConnector}
/>
<Route
path="/wanted/cutoffunmet"
component={CutoffUnmetConnector}
component={BlocklistConnector}
/>
{/*

View File

@@ -65,12 +65,12 @@ function AppUpdatedModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('AppUpdated')}
{translate('AppUpdated', { appName: 'Radarr' })}
</ModalHeader>
<ModalBody>
<div>
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
<InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Radarr', version })} blockClassName={styles.version} />
</div>
{

View File

@@ -0,0 +1,49 @@
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);

View File

@@ -1,33 +0,0 @@
import { useCallback, useEffect } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
import AppState from './State/AppState';
function createThemeSelector() {
return createSelector(
(state: AppState) => state.settings.ui.item.theme || window.Radarr.theme,
(theme) => {
return theme;
}
);
}
function ApplyTheme() {
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 null;
}
export default ApplyTheme;

View File

@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
<ModalBody>
<div>
{translate('ConnectionLostToBackend')}
{translate('ConnectionLostToBackend', { appName: 'Radarr' })}
</div>
<div className={styles.automatic}>
{translate('ConnectionLostReconnect')}
{translate('ConnectionLostReconnect', { appName: 'Radarr' })}
</div>
</ModalBody>
<ModalFooter>

View File

@@ -1,6 +1,4 @@
import Column from 'Components/Table/Column';
import SortDirection from 'Helpers/Props/SortDirection';
import { FilterBuilderProp, PropertyFilter } from './AppState';
export interface Error {
responseJSON: {
@@ -19,19 +17,7 @@ export interface AppSectionSaveState {
}
export interface PagedAppSectionState {
page: number;
pageSize: number;
totalPages: number;
totalRecords?: number;
}
export interface TableAppSectionState {
columns: Column[];
}
export interface AppSectionFilterState<T> {
selectedFilterKey: string;
filters: PropertyFilter[];
filterBuilderProps: FilterBuilderProp<T>[];
}
export interface AppSectionSchemaState<T> {
@@ -47,7 +33,6 @@ export interface AppSectionItemState<T> {
isFetching: boolean;
isPopulated: boolean;
error: Error;
pendingChanges: Partial<T>;
item: T;
}

View File

@@ -1,10 +1,7 @@
import BlocklistAppState from './BlocklistAppState';
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState';
import InteractiveImportAppState from './InteractiveImportAppState';
import MovieCollectionAppState from './MovieCollectionAppState';
import MovieCreditAppState from './MovieCreditAppState';
import MovieFilesAppState from './MovieFilesAppState';
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
import ParseAppState from './ParseAppState';
@@ -46,26 +43,11 @@ export interface CustomFilter {
filers: PropertyFilter[];
}
export interface AppSectionState {
isConnected: boolean;
isReconnecting: boolean;
version: string;
dimensions: {
isSmallScreen: boolean;
width: number;
height: number;
};
}
interface AppState {
app: AppSectionState;
blocklist: BlocklistAppState;
calendar: CalendarAppState;
commands: CommandAppState;
history: HistoryAppState;
interactiveImport: InteractiveImportAppState;
movieCollections: MovieCollectionAppState;
movieCredits: MovieCreditAppState;
movieFiles: MovieFilesAppState;
movieIndex: MovieIndexAppState;
movies: MoviesAppState;

View File

@@ -1,16 +0,0 @@
import Blocklist from 'typings/Blocklist';
import AppSectionState, {
AppSectionFilterState,
PagedAppSectionState,
TableAppSectionState,
} from './AppSectionState';
interface BlocklistAppState
extends AppSectionState<Blocklist>,
AppSectionFilterState<Blocklist>,
PagedAppSectionState,
TableAppSectionState {
isRemoving: boolean;
}
export default BlocklistAppState;

View File

@@ -1,10 +1,9 @@
import AppSectionState, {
AppSectionFilterState,
} from 'App/State/AppSectionState';
import AppSectionState from 'App/State/AppSectionState';
import Movie from 'Movie/Movie';
import { FilterBuilderProp } from './AppState';
interface CalendarAppState
extends AppSectionState<Movie>,
AppSectionFilterState<Movie> {}
interface CalendarAppState extends AppSectionState<Movie> {
filterBuilderProps: FilterBuilderProp<Movie>[];
}
export default CalendarAppState;

View File

@@ -1,10 +0,0 @@
import AppSectionState, {
AppSectionFilterState,
} from 'App/State/AppSectionState';
import History from 'typings/History';
interface HistoryAppState
extends AppSectionState<History>,
AppSectionFilterState<History> {}
export default HistoryAppState;

View File

@@ -1,8 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import MovieCollection from 'typings/MovieCollection';
interface MovieCollectionAppState extends AppSectionState<MovieCollection> {
itemMap: Record<number, number>;
}
type MovieCollectionAppState = AppSectionState<MovieCollection>;
export default MovieCollectionAppState;

View File

@@ -1,6 +0,0 @@
import AppSectionState from 'App/State/AppSectionState';
import MovieCredit from 'typings/MovieCredit';
interface MovieCreditAppState extends AppSectionState<MovieCredit> {}
export default MovieCreditAppState;

View File

@@ -20,14 +20,11 @@ export interface MovieIndexAppState {
showTitle: boolean;
showMonitored: boolean;
showQualityProfile: boolean;
showCinemaRelease: boolean;
showDigitalRelease: boolean;
showPhysicalRelease: boolean;
showReleaseDate: boolean;
showCinemaRelease: boolean;
showTmdbRating: boolean;
showImdbRating: boolean;
showRottenTomatoesRating: boolean;
showTags: boolean;
showSearchAction: boolean;
};
@@ -40,7 +37,6 @@ export interface MovieIndexAppState {
showAdded: boolean;
showPath: boolean;
showSizeOnDisk: boolean;
showTags: boolean;
showSearchAction: boolean;
};

View File

@@ -1,31 +1,41 @@
import Queue from 'typings/Queue';
import AppSectionState, {
AppSectionFilterState,
AppSectionItemState,
Error,
PagedAppSectionState,
TableAppSectionState,
} from './AppSectionState';
import ModelBase from 'App/ModelBase';
import Language from 'Language/Language';
import { QualityModel } from 'Quality/Quality';
import CustomFormat from 'typings/CustomFormat';
import AppSectionState, { AppSectionItemState, Error } from './AppSectionState';
export interface QueueStatus {
totalCount: number;
count: number;
unknownCount: number;
errors: boolean;
warnings: boolean;
unknownErrors: boolean;
unknownWarnings: boolean;
export interface StatusMessage {
title: string;
messages: string[];
}
export interface Queue extends ModelBase {
languages: Language[];
quality: QualityModel;
customFormats: CustomFormat[];
size: number;
title: string;
sizeleft: number;
timeleft: string;
estimatedCompletionTime: string;
status: string;
trackedDownloadStatus: string;
trackedDownloadState: string;
statusMessages: StatusMessage[];
errorMessage: string;
downloadId: string;
protocol: string;
downloadClient: string;
outputPath: string;
movieHasFile: boolean;
movieId?: number;
}
export interface QueueDetailsAppState extends AppSectionState<Queue> {
params: unknown;
}
export interface QueuePagedAppState
extends AppSectionState<Queue>,
AppSectionFilterState<Queue>,
PagedAppSectionState,
TableAppSectionState {
export interface QueuePagedAppState extends AppSectionState<Queue> {
isGrabbing: boolean;
grabError: Error;
isRemoving: boolean;
@@ -33,12 +43,9 @@ export interface QueuePagedAppState
}
interface QueueAppState {
status: AppSectionItemState<QueueStatus>;
status: AppSectionItemState<Queue>;
details: QueueDetailsAppState;
paged: QueuePagedAppState;
options: {
includeUnknownMovieItems: boolean;
};
}
export default QueueAppState;

View File

@@ -3,30 +3,19 @@ import AppSectionState, {
AppSectionItemState,
AppSectionSaveState,
AppSectionSchemaState,
PagedAppSectionState,
} from 'App/State/AppSectionState';
import Language from 'Language/Language';
import CustomFormat from 'typings/CustomFormat';
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import ImportListExclusion from 'typings/ImportListExclusion';
import ImportListOptionsSettings from 'typings/ImportListOptionsSettings';
import Indexer from 'typings/Indexer';
import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile';
import General from 'typings/Settings/General';
import UiSettings from 'typings/Settings/UiSettings';
import { UiSettings } from 'typings/UiSettings';
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
AppSectionSaveState {
isTestingAll: boolean;
}
export interface GeneralAppState
extends AppSectionItemState<General>,
AppSectionSaveState {}
export interface ImportListAppState
@@ -37,9 +26,7 @@ export interface ImportListAppState
export interface IndexerAppState
extends AppSectionState<Indexer>,
AppSectionDeleteState,
AppSectionSaveState {
isTestingAll: boolean;
}
AppSectionSaveState {}
export interface NotificationAppState
extends AppSectionState<Notification>,
@@ -49,34 +36,12 @@ export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>,
AppSectionSchemaState<QualityProfile> {}
export interface CustomFormatAppState
extends AppSectionState<CustomFormat>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface ImportListOptionsSettingsAppState
extends AppSectionItemState<ImportListOptionsSettings>,
AppSectionSaveState {}
export interface ImportListExclusionsSettingsAppState
extends AppSectionState<ImportListExclusion>,
AppSectionSaveState,
PagedAppSectionState,
AppSectionDeleteState {
pendingChanges: Partial<ImportListExclusion>;
}
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
export type LanguageSettingsAppState = AppSectionState<Language>;
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState {
advancedSettings: boolean;
customFormats: CustomFormatAppState;
downloadClients: DownloadClientAppState;
general: GeneralAppState;
importListExclusions: ImportListExclusionsSettingsAppState;
importListOptions: ImportListOptionsSettingsAppState;
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState;

View File

@@ -1,19 +1,10 @@
import DiskSpace from 'typings/DiskSpace';
import Health from 'typings/Health';
import SystemStatus from 'typings/SystemStatus';
import Task from 'typings/Task';
import AppSectionState, { AppSectionItemState } from './AppSectionState';
import { AppSectionItemState } from './AppSectionState';
export type DiskSpaceAppState = AppSectionState<DiskSpace>;
export type HealthAppState = AppSectionState<Health>;
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
export type TaskAppState = AppSectionState<Task>;
interface SystemAppState {
diskSpace: DiskSpaceAppState;
health: HealthAppState;
status: SystemStatusAppState;
tasks: TaskAppState;
}
export default SystemAppState;

View File

@@ -42,9 +42,9 @@ function Agenda(props) {
<div className={styles.agenda}>
{
items.map((item, index) => {
const momentDate = moment(item.sortDate);
const momentDate = moment(item.inCinemas);
const showDate = index === 0 ||
!moment(items[index - 1].sortDate).isSame(momentDate, 'day');
!moment(items[index - 1].inCinemas).isSame(momentDate, 'day');
return (
<AgendaEventConnector

View File

@@ -88,7 +88,7 @@
}
@media only screen and (max-width: $breakpointSmall) {
.overlay {
.event {
flex-direction: column;
}
@@ -111,4 +111,5 @@
.releaseIcon {
margin-right: 20px;
width: 25px;
text-align: right;
}

View File

@@ -3,10 +3,10 @@ import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
import getStatusStyle from 'Calendar/getStatusStyle';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons, kinds } from 'Helpers/Props';
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
import translate from 'Utilities/String/translate';
import styles from './AgendaEvent.css';
@@ -82,7 +82,7 @@ class AgendaEvent extends Component {
startTime = moment(startTime);
const downloading = !!(queueItem || grabbed);
const isMonitored = monitored;
const statusStyle = getStatusStyle(hasFile, downloading, isMonitored, isAvailable);
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', downloading);
const joinedGenres = genres.slice(0, 2).join(', ');
const link = `/movie/${titleSlug}`;
@@ -95,7 +95,7 @@ class AgendaEvent extends Component {
<div className={styles.overlay}>
<div className={styles.date}>
{showDate ? startTime.format(longDateFormat) : null}
{(showDate) ? startTime.format(longDateFormat) : null}
</div>
<div className={styles.releaseIcon}>
@@ -147,7 +147,7 @@ class AgendaEvent extends Component {
className={styles.statusIcon}
name={icons.MOVIE_FILE}
kind={kinds.WARNING}
title={translate('QualityCutoffNotMet')}
title={translate('QualityCutoffHasNotBeenMet')}
/>
}
</div>

View File

@@ -33,7 +33,9 @@ class Calendar extends Component {
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>{translate('CalendarLoadError')}</Alert>
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadTheCalendar')}
</Alert>
}
{

View File

@@ -55,7 +55,7 @@ class CalendarConnector extends Component {
gotoCalendarToday
} = this.props;
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchCalendar();

View File

@@ -23,11 +23,13 @@ function createFilterBuilderPropsSelector() {
);
}
interface CalendarFilterModalProps {
interface SeriesIndexFilterModalProps {
isOpen: boolean;
}
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
export default function CalendarFilterModal(
props: SeriesIndexFilterModalProps
) {
const sectionItems = useSelector(createCalendarSelector());
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'calendar';

View File

@@ -104,7 +104,7 @@ class CalendarPage extends Component {
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('ICalLink')}
label={translate('iCalLink')}
iconName={icons.CALENDAR}
onPress={this.onGetCalendarLinkPress}
/>
@@ -112,7 +112,7 @@ class CalendarPage extends Component {
<PageToolbarSeparator />
<PageToolbarButton
label={translate('RssSync')}
label={translate('RSSSync')}
iconName={icons.RSS}
isSpinning={isRssSyncExecuting}
onPress={onRssSyncPress}
@@ -180,7 +180,7 @@ class CalendarPage extends Component {
{
!movieError && movieIsPopulated && !hasMovie &&
<NoMovie totalItems={0} />
<NoMovie />
}
{

View File

@@ -28,7 +28,7 @@ class DayOfWeek extends Component {
if (view === calendarViews.WEEK) {
formatedDate = momentDate.format(calendarWeekColumnHeader);
} else if (view === calendarViews.FORECAST) {
formatedDate = getRelativeDate({ date, shortDateFormat, showRelativeDates });
formatedDate = getRelativeDate(date, shortDateFormat, showRelativeDates);
}
return (

View File

@@ -48,10 +48,6 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
.statusContainer {
display: flex;
align-items: center;
&:global(.fullColor) {
filter: var(--calendarFullColorFilter)
}
}
.statusIcon {

View File

@@ -2,10 +2,10 @@ import classNames from 'classnames';
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getStatusStyle from 'Calendar/getStatusStyle';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons, kinds } from 'Helpers/Props';
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
import translate from 'Utilities/String/translate';
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
import styles from './CalendarEvent.css';
@@ -39,7 +39,7 @@ class CalendarEvent extends Component {
const isDownloading = !!(queueItem || grabbed);
const isMonitored = monitored;
const statusStyle = getStatusStyle(hasFile, isDownloading, isMonitored, isAvailable);
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', isDownloading);
const joinedGenres = genres.slice(0, 2).join(', ');
const link = `/movie/${titleSlug}`;
const eventType = [];
@@ -76,18 +76,12 @@ class CalendarEvent extends Component {
{title}
</div>
<div
className={classNames(
styles.statusContainer,
fullColorEvents && 'fullColor'
)}
>
<div className={styles.statusContainer}>
{
queueItem ?
<span className={styles.statusIcon}>
<CalendarEventQueueDetails
{...queueItem}
fullColorEvents={fullColorEvents}
/>
</span> :
null
@@ -104,14 +98,12 @@ class CalendarEvent extends Component {
}
{
showCutoffUnmetIcon &&
!!movieFile &&
movieFile.qualityCutoffNotMet ?
showCutoffUnmetIcon && !!movieFile && movieFile.qualityCutoffNotMet ?
<Icon
className={styles.statusIcon}
name={icons.MOVIE_FILE}
kind={kinds.WARNING}
title={translate('QualityCutoffNotMet')}
title={translate('QualityCutoffHasNotBeenMet')}
/> :
null
}

Some files were not shown because too many files have changed in this diff Show More