mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-26 17:44:24 -04:00
Compare commits
84 Commits
v5.17.2.95
...
v5.20.0.97
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
653b358fd3 | ||
|
|
6a7ed22b44 | ||
|
|
779292490a | ||
|
|
e4e96fc7f9 | ||
|
|
049bf7715e | ||
|
|
df4dfaac0b | ||
|
|
89c96b0a80 | ||
|
|
7db12b6e58 | ||
|
|
28dee7bc01 | ||
|
|
8ec60eb0a6 | ||
|
|
102849a697 | ||
|
|
95da7d7b47 | ||
|
|
22b5739967 | ||
|
|
cfba047d80 | ||
|
|
576d404e70 | ||
|
|
5959d4e51a | ||
|
|
2aca6c6e1d | ||
|
|
e8bbe0ee9f | ||
|
|
66332a110a | ||
|
|
36c66deb4b | ||
|
|
edec432244 | ||
|
|
554e15d438 | ||
|
|
553645a07c | ||
|
|
7de7e83c5b | ||
|
|
b7a46bedb0 | ||
|
|
0925769377 | ||
|
|
72244362fe | ||
|
|
c6526c34e9 | ||
|
|
efa2913dbc | ||
|
|
35c22a4ffa | ||
|
|
66d96e21da | ||
|
|
36d4e9e6cd | ||
|
|
7189d7b15c | ||
|
|
6e80113987 | ||
|
|
bb8a0dda63 | ||
|
|
525ed65687 | ||
|
|
3fbccc6af3 | ||
|
|
8e10eecfac | ||
|
|
a3b1512552 | ||
|
|
d375b5ffbe | ||
|
|
884abc0368 | ||
|
|
f8da7aae03 | ||
|
|
c165118d4d | ||
|
|
b3dd571a92 | ||
|
|
dd900eb739 | ||
|
|
66aae0c91c | ||
|
|
d888a0a2b3 | ||
|
|
cb5416a18c | ||
|
|
7977e0be05 | ||
|
|
cd836fef38 | ||
|
|
b0bfbe767c | ||
|
|
528b93dabe | ||
|
|
1edcbee5e1 | ||
|
|
8853dced9f | ||
|
|
c7aa1bae5e | ||
|
|
405ae77070 | ||
|
|
6236bc9b4f | ||
|
|
743c977e5b | ||
|
|
c0e5646f07 | ||
|
|
10094b4e66 | ||
|
|
d923406f08 | ||
|
|
69a9c72286 | ||
|
|
55b9477a01 | ||
|
|
6b81f92137 | ||
|
|
3ceda1bcda | ||
|
|
f1f1921517 | ||
|
|
af0c96538a | ||
|
|
3d52f45b6a | ||
|
|
d4715f119d | ||
|
|
d58135bf17 | ||
|
|
b452c10da3 | ||
|
|
f6b364725d | ||
|
|
99f6be3f3d | ||
|
|
c2ac49a873 | ||
|
|
0e24a3e8bc | ||
|
|
18032cc83b | ||
|
|
927eb38945 | ||
|
|
5fac348613 | ||
|
|
7ba9603449 | ||
|
|
e36de8ab8d | ||
|
|
f8704a1655 | ||
|
|
f507d5154e | ||
|
|
5f03e7142a | ||
|
|
c0ebbee7c9 |
@@ -6,7 +6,7 @@
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"nodeGypDependencies": true,
|
||||
"version": "16",
|
||||
"version": "20",
|
||||
"nvmVersion": "latest"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -87,4 +87,4 @@ This project is also supported by DigitalOcean
|
||||
### License
|
||||
|
||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
* Copyright 2010-2024
|
||||
* Copyright 2010-2025
|
||||
|
||||
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '5.17.2'
|
||||
majorVersion: '5.20.0'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
@@ -1116,20 +1116,20 @@ stages:
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
steps:
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
- task: SonarCloudPrepare@2
|
||||
- task: SonarCloudPrepare@3
|
||||
env:
|
||||
SONAR_SCANNER_OPTS: ''
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'radarr'
|
||||
scannerMode: 'CLI'
|
||||
scannerMode: 'cli'
|
||||
configMode: 'manual'
|
||||
cliProjectKey: 'Radarr_Radarr.UI'
|
||||
cliProjectName: 'RadarrUI'
|
||||
cliProjectVersion: '$(radarrVersion)'
|
||||
cliSources: './frontend'
|
||||
- task: SonarCloudAnalyze@2
|
||||
|
||||
- task: SonarCloudAnalyze@3
|
||||
|
||||
- job: Api_Docs
|
||||
displayName: API Docs
|
||||
dependsOn: Prepare
|
||||
@@ -1205,12 +1205,12 @@ stages:
|
||||
submodules: true
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
- task: SonarCloudPrepare@2
|
||||
- task: SonarCloudPrepare@3
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'radarr'
|
||||
scannerMode: 'MSBuild'
|
||||
scannerMode: 'dotnet'
|
||||
projectKey: 'Radarr_Radarr'
|
||||
projectName: 'Radarr'
|
||||
projectVersion: '$(radarrVersion)'
|
||||
@@ -1223,7 +1223,7 @@ 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@3
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
displayName: Publish SonarCloud Results
|
||||
- task: reportgenerator@5.3.11
|
||||
|
||||
15
docs.sh
15
docs.sh
@@ -1,13 +1,18 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
FRAMEWORK="net6.0"
|
||||
PLATFORM=$1
|
||||
ARCHITECTURE="${2:-x64}"
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
RUNTIME="win-x64"
|
||||
RUNTIME="win-$ARCHITECTURE"
|
||||
elif [ "$PLATFORM" = "Linux" ]; then
|
||||
RUNTIME="linux-x64"
|
||||
RUNTIME="linux-$ARCHITECTURE"
|
||||
elif [ "$PLATFORM" = "Mac" ]; then
|
||||
RUNTIME="osx-x64"
|
||||
RUNTIME="osx-$ARCHITECTURE"
|
||||
else
|
||||
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
||||
echo "Platform must be provided as first argument: Windows, Linux or Mac"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -35,7 +40,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.6.2 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/$FRAMEWORK/$RUNTIME/$application" v3 &
|
||||
|
||||
sleep 45
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ function HistoryDetails(props: HistoryDetailsProps) {
|
||||
indexer,
|
||||
releaseGroup,
|
||||
movieMatchType,
|
||||
releaseSource,
|
||||
customFormatScore,
|
||||
nzbInfoUrl,
|
||||
downloadClient,
|
||||
@@ -53,6 +54,31 @@ function HistoryDetails(props: HistoryDetailsProps) {
|
||||
|
||||
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
||||
|
||||
let releaseSourceMessage = '';
|
||||
|
||||
switch (releaseSource) {
|
||||
case 'Unknown':
|
||||
releaseSourceMessage = translate('Unknown');
|
||||
break;
|
||||
case 'Rss':
|
||||
releaseSourceMessage = translate('Rss');
|
||||
break;
|
||||
case 'Search':
|
||||
releaseSourceMessage = translate('Search');
|
||||
break;
|
||||
case 'UserInvokedSearch':
|
||||
releaseSourceMessage = translate('UserInvokedSearch');
|
||||
break;
|
||||
case 'InteractiveSearch':
|
||||
releaseSourceMessage = translate('InteractiveSearch');
|
||||
break;
|
||||
case 'ReleasePush':
|
||||
releaseSourceMessage = translate('ReleasePush');
|
||||
break;
|
||||
default:
|
||||
releaseSourceMessage = '';
|
||||
}
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
@@ -88,6 +114,14 @@ function HistoryDetails(props: HistoryDetailsProps) {
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{releaseSource ? (
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('ReleaseSource')}
|
||||
data={releaseSourceMessage}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{nzbInfoUrl ? (
|
||||
<span>
|
||||
<DescriptionListItemTitle>
|
||||
|
||||
@@ -37,7 +37,7 @@ interface HistoryDetailsModalProps {
|
||||
sourceTitle: string;
|
||||
data: HistoryData;
|
||||
downloadId?: string;
|
||||
isMarkingAsFailed: boolean;
|
||||
isMarkingAsFailed?: boolean;
|
||||
onMarkAsFailedPress: () => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,6 @@ ImportMovieRow.propTypes = {
|
||||
selectedMovie: PropTypes.object,
|
||||
isExistingMovie: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
queued: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired
|
||||
|
||||
@@ -131,7 +131,7 @@ class ImportMovieSelectMovie extends Component {
|
||||
id={this._buttonId}
|
||||
>
|
||||
<Link
|
||||
ref={ref}
|
||||
// ref={ref}
|
||||
className={styles.button}
|
||||
component="div"
|
||||
onPress={this.onPress}
|
||||
@@ -255,7 +255,7 @@ class ImportMovieSelectMovie extends Component {
|
||||
items.map((item) => {
|
||||
return (
|
||||
<ImportMovieSearchResultConnector
|
||||
key={item.tvdbId}
|
||||
key={item.tmdbId}
|
||||
tmdbId={item.tmdbId}
|
||||
title={item.title}
|
||||
year={item.year}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import BlocklistAppState from './BlocklistAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import HistoryAppState from './HistoryAppState';
|
||||
import HistoryAppState, { MovieHistoryAppState } from './HistoryAppState';
|
||||
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||
import MovieBlocklistAppState from './MovieBlocklistAppState';
|
||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||
import MovieCreditAppState from './MovieCreditAppState';
|
||||
import MovieFilesAppState from './MovieFilesAppState';
|
||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||
import OrganizePreviewAppState from './OrganizePreviewAppState';
|
||||
import ParseAppState from './ParseAppState';
|
||||
import PathsAppState from './PathsAppState';
|
||||
import QueueAppState from './QueueAppState';
|
||||
import ReleasesAppState from './ReleasesAppState';
|
||||
import RootFolderAppState from './RootFolderAppState';
|
||||
import SettingsAppState from './SettingsAppState';
|
||||
import SystemAppState from './SystemAppState';
|
||||
@@ -66,14 +69,18 @@ interface AppState {
|
||||
commands: CommandAppState;
|
||||
history: HistoryAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
movieBlocklist: MovieBlocklistAppState;
|
||||
movieCollections: MovieCollectionAppState;
|
||||
movieCredits: MovieCreditAppState;
|
||||
movieFiles: MovieFilesAppState;
|
||||
movieHistory: MovieHistoryAppState;
|
||||
movieIndex: MovieIndexAppState;
|
||||
movies: MoviesAppState;
|
||||
organizePreview: OrganizePreviewAppState;
|
||||
parse: ParseAppState;
|
||||
paths: PathsAppState;
|
||||
queue: QueueAppState;
|
||||
releases: ReleasesAppState;
|
||||
rootFolders: RootFolderAppState;
|
||||
settings: SettingsAppState;
|
||||
system: SystemAppState;
|
||||
|
||||
@@ -5,6 +5,8 @@ import AppSectionState, {
|
||||
} from 'App/State/AppSectionState';
|
||||
import History from 'typings/History';
|
||||
|
||||
export type MovieHistoryAppState = AppSectionState<History>;
|
||||
|
||||
interface HistoryAppState
|
||||
extends AppSectionState<History>,
|
||||
AppSectionFilterState<History>,
|
||||
|
||||
6
frontend/src/App/State/MovieBlocklistAppState.ts
Normal file
6
frontend/src/App/State/MovieBlocklistAppState.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Blocklist from 'typings/Blocklist';
|
||||
|
||||
type MovieBlocklistAppState = AppSectionState<Blocklist>;
|
||||
|
||||
export default MovieBlocklistAppState;
|
||||
@@ -1,7 +1,11 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import AppSectionState, {
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import MovieCollection from 'typings/MovieCollection';
|
||||
|
||||
interface MovieCollectionAppState extends AppSectionState<MovieCollection> {
|
||||
interface MovieCollectionAppState
|
||||
extends AppSectionState<MovieCollection>,
|
||||
AppSectionSaveState {
|
||||
itemMap: Record<number, number>;
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,8 @@ interface MoviesAppState
|
||||
deleteOptions: {
|
||||
addImportExclusion: boolean;
|
||||
};
|
||||
|
||||
pendingChanges: Partial<Movie>;
|
||||
}
|
||||
|
||||
export default MoviesAppState;
|
||||
|
||||
13
frontend/src/App/State/OrganizePreviewAppState.ts
Normal file
13
frontend/src/App/State/OrganizePreviewAppState.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
|
||||
export interface OrganizePreviewModel extends ModelBase {
|
||||
movieId: number;
|
||||
movieFileId: number;
|
||||
existingPath: string;
|
||||
newPath: string;
|
||||
}
|
||||
|
||||
type OrganizePreviewAppState = AppSectionState<OrganizePreviewModel>;
|
||||
|
||||
export default OrganizePreviewAppState;
|
||||
10
frontend/src/App/State/ReleasesAppState.ts
Normal file
10
frontend/src/App/State/ReleasesAppState.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Release from 'typings/Release';
|
||||
|
||||
interface ReleasesAppState
|
||||
extends AppSectionState<Release>,
|
||||
AppSectionFilterState<Release> {}
|
||||
|
||||
export default ReleasesAppState;
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import EditMovieModal from 'Movie/Edit/EditMovieModal';
|
||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -172,7 +172,7 @@ class CollectionMovie extends Component {
|
||||
collectionId={collectionId}
|
||||
/>
|
||||
|
||||
<EditMovieModalConnector
|
||||
<EditMovieModal
|
||||
isOpen={isEditMovieModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onEditMovieModalClose}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
|
||||
type PropertyFunction<T> = () => T;
|
||||
|
||||
@@ -9,6 +10,7 @@ interface Column {
|
||||
className?: string;
|
||||
columnLabel?: string;
|
||||
isSortable?: boolean;
|
||||
fixedSortDirection?: SortDirection;
|
||||
isVisible: boolean;
|
||||
isModifiable?: boolean;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ function TraktRating(props: TraktRatingProps) {
|
||||
const { ratings, iconSize = 14, hideIcon = false } = props;
|
||||
|
||||
const traktImage =
|
||||
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiAgICAgdmlld0JveD0iMCAwIDE0NC44IDE0NC44IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxNDQuOCAxNDQuOCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+ICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik0yOS41LDExMS44YzEwLjYsMTEuNiwyNS45LDE4LjgsNDIuOSwxOC44YzguNywwLDE2LjktMS45LDI0LjMtNS4zTDU2LjMsODVMMjkuNSwxMTEuOHoiLz4gICAgPHBhdGggZmlsbD0iI0VEMjIyNCIgZD0iTTU2LjEsNjAuNkwyNS41LDkxLjFMMjEuNCw4N2wzMi4yLTMyLjJoMGwzNy42LTM3LjZjLTUuOS0yLTEyLjItMy4xLTE4LjgtMy4xYy0zMi4yLDAtNTguMywyNi4xLTU4LjMsNTguMyAgICAgICBjMCwxMy4xLDQuMywyNS4yLDExLjcsMzVsMzAuNS0zMC41bDIuMSwybDQzLjcsNDMuN2MwLjktMC41LDEuNy0xLDIuNS0xLjZMNTYuMyw3Mi43TDI3LDEwMmwtNC4xLTQuMWwzMy40LTMzLjRsMi4xLDJsNTEsNTAuOSAgICAgICBjMC44LTAuNiwxLjUtMS4zLDIuMi0xLjlsLTU1LTU1TDU2LjEsNjAuNnoiLz4gICAgPHBhdGggZmlsbD0iI0VEMUMyNCIgZD0iTTExNS43LDExMS40YzkuMy0xMC4zLDE1LTI0LDE1LTM5YzAtMjMuNC0xMy44LTQzLjUtMzMuNi01Mi44TDYwLjQsNTYuMkwxMTUuNywxMTEuNHogTTc0LjUsNjYuOGwtNC4xLTQuMSAgICAgICBsMjguOS0yOC45bDQuMSw0LjFMNzQuNSw2Ni44eiBNMTAxLjksMjcuMUw2OC42LDYwLjRsLTQuMS00LjFMOTcuOCwyM0wxMDEuOSwyNy4xeiIvPiAgICA8Zz4gICAgICAgPGc+ICAgICAgICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik03Mi40LDE0NC44QzMyLjUsMTQ0LjgsMCwxMTIuMywwLDcyLjRDMCwzMi41LDMyLjUsMCw3Mi40LDBzNzIuNCwzMi41LDcyLjQsNzIuNCAgICAgICAgICAgICBDMTQ0LjgsMTEyLjMsMTEyLjMsMTQ0LjgsNzIuNCwxNDQuOHogTTcyLjQsNy4zQzM2LjUsNy4zLDcuMywzNi41LDcuMyw3Mi40czI5LjIsNjUuMSw2NS4xLDY1LjFzNjUuMS0yOS4yLDY1LjEtNjUuMSAgICAgICAgICAgICBTMTA4LjMsNy4zLDcyLjQsNy4zeiIvPiAgICAgICA8L2c+ICAgIDwvZz48L2c+PC9zdmc+';
|
||||
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyBpZD0iTGF5ZXJfMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2aWV3Qm94PSIwIDAgNDggNDgiPgogIDxkZWZzPgogICAgPHN0eWxlPgogICAgICAuY2xzLTEgewogICAgICAgIGZpbGw6ICM5ZjQyYzY7CiAgICAgIH0KCiAgICAgIC5jbHMtMiB7CiAgICAgICAgZmlsbDogI2ZmZjsKICAgICAgfQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPGcgaWQ9Il94MkRfLXByb2R1Y3Rpb24iPgogICAgPGcgaWQ9ImxvZ29tYXJrLmNpcmNsZS5jb2xvciI+CiAgICAgIDxwYXRoIGlkPSJiYWNrZ3JvdW5kIiBjbGFzcz0iY2xzLTEiIGQ9Ik00OCwyNGMwLDYuNjItMi42OSwxMi42Mi03LjAzLDE2Ljk3LTQuMzQsNC4zNC0xMC4zNCw3LjAzLTE2Ljk3LDcuMDNDMTAuNzUsNDgsMCwzNy4yNSwwLDI0YzAtNi42MywyLjY5LTEyLjYzLDcuMDMtMTYuOTdDMTEuMzcsMi42OCwxNy4zNywwLDI0LDBzMTIuNjMsMi42OCwxNi45Nyw3LjAzYy4xNC4xNC4yNy4yOC40LjQyLjQ4LjUuOTQsMS4wMiwxLjM3LDEuNTYuMjEuMjYuNDEuNTIuNi43OS40My41Ny44MiwxLjE2LDEuMTgsMS43Ni4xOC4yOS4zNS41OC41MS44Ny4zNS42NC42OCwxLjI5Ljk2LDEuOTcsMS4zLDIuOTQsMi4wMSw2LjE4LDIuMDEsOS42WiIvPgogICAgICA8ZyBpZD0iY2hlY2tib3giPgogICAgICAgIDxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTEyLjkzLDE4LjY3bC0xLjQ3LDEuNDYsMTQuNCwxNC40LDEuNDctMS40Ny00LjMyLTQuMzEsMTkuNzMtMTkuNzRjLS40My0uNTQtLjg5LTEuMDYtMS4zNy0xLjU2bC0xOS44MywxOS44My04LjYxLTguNjFaTTI4LjAyLDMyLjM3bDEuNDYtMS40Ni0yLjE1LTIuMTYsMTcuMTktMTcuMTljLS4zNi0uNi0uNzUtMS4xOS0xLjE4LTEuNzZsLTE4Ljk0LDE4Ljk1LDMuNjIsMy42MlpNMzAuMTgsMzAuMjFsMTUuODEtMTUuODFjLS4yOC0uNjgtLjYxLTEuMzMtLjk2LTEuOTdsLTE2LjMyLDE2LjMyLDEuNDcsMS40NlpNMTMuNjIsMTcuOTdsNy45Miw3LjkyLDEuNDctMS40Ny03LjkyLTcuOTItMS40NywxLjQ3Wk0yNS4xNywyMi4yN2wtNy45Mi03LjkyLTEuNDcsMS40Nyw3LjkyLDcuOTIsMS40Ny0xLjQ3Wk0yNCw0MS4zMmMtOS41NSwwLTE3LjMyLTcuNzctMTcuMzItMTcuMzJTMTQuNDUsNi42NywyNCw2LjY3YzIuNiwwLDUuMTEuNTYsNy40NCwxLjY4bC44OS0xLjg3Yy0yLjYxLTEuMjUtNS40Mi0xLjg4LTguMzMtMS44OEMxMy4zMSw0LjYsNC42MSwxMy4zLDQuNjEsMjRzOC43LDE5LjQsMTkuNCwxOS40YzcuNjQsMCwxNC41OS00LjUxLDE3LjcxLTExLjQ4bC0xLjg5LS44NWMtMi43OSw2LjIzLTksMTAuMjYtMTUuODIsMTAuMjZaIi8+CiAgICAgIDwvZz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPg==';
|
||||
|
||||
const { value = 0, votes = 0 } = ratings.trakt;
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
reprocessInteractiveImportItems,
|
||||
updateInteractiveImportItem,
|
||||
} from 'Store/Actions/interactiveImportActions';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import Rejection from 'typings/Rejection';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
@@ -52,7 +53,7 @@ interface InteractiveImportRowProps {
|
||||
quality?: QualityModel;
|
||||
languages?: Language[];
|
||||
size: number;
|
||||
customFormats?: object[];
|
||||
customFormats?: CustomFormat[];
|
||||
customFormatScore?: number;
|
||||
indexerFlags: number;
|
||||
rejections: Rejection[];
|
||||
|
||||
@@ -2,6 +2,7 @@ import ModelBase from 'App/ModelBase';
|
||||
import Language from 'Language/Language';
|
||||
import Movie from 'Movie/Movie';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import Rejection from 'typings/Rejection';
|
||||
|
||||
export interface InteractiveImportCommandOptions {
|
||||
@@ -27,7 +28,7 @@ interface InteractiveImport extends ModelBase {
|
||||
languages: Language[];
|
||||
movie?: Movie;
|
||||
qualityWeight: number;
|
||||
customFormats: object[];
|
||||
customFormats: CustomFormat[];
|
||||
indexerFlags: number;
|
||||
rejections: Rejection[];
|
||||
movieFileId?: number;
|
||||
|
||||
@@ -65,7 +65,7 @@ interface RowItemData {
|
||||
}
|
||||
|
||||
function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
|
||||
const { items, columns, onMovieSelect } = data;
|
||||
const { items, onMovieSelect } = data;
|
||||
const movie = index >= items.length ? null : items[index];
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
@@ -88,13 +88,11 @@ function Row({ index, style, data }: ListChildComponentProps<RowItemData>) {
|
||||
onPress={handlePress}
|
||||
>
|
||||
<SelectMovieRow
|
||||
id={movie.id}
|
||||
key={movie.id}
|
||||
title={movie.title}
|
||||
tmdbId={movie.tmdbId}
|
||||
imdbId={movie.imdbId}
|
||||
year={movie.year}
|
||||
columns={columns}
|
||||
onMovieSelect={onMovieSelect}
|
||||
/>
|
||||
</VirtualTableRowButton>
|
||||
);
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import styles from './SelectMovieRow.css';
|
||||
|
||||
class SelectMovieRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
this.props.onMovieSelect(this.props.id);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<VirtualTableRowCell className={styles.title}>
|
||||
{this.props.title}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.year}>
|
||||
{this.props.year}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.imdbId}>
|
||||
{
|
||||
this.props.imdbId ?
|
||||
<Label>{this.props.imdbId}</Label> :
|
||||
null
|
||||
}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.tmdbId}>
|
||||
<Label>{this.props.tmdbId}</Label>
|
||||
</VirtualTableRowCell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectMovieRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
imdbId: PropTypes.string,
|
||||
year: PropTypes.number.isRequired,
|
||||
onMovieSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectMovieRow;
|
||||
33
frontend/src/InteractiveImport/Movie/SelectMovieRow.tsx
Normal file
33
frontend/src/InteractiveImport/Movie/SelectMovieRow.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import styles from './SelectMovieRow.css';
|
||||
|
||||
interface SelectMovieRowProps {
|
||||
title: string;
|
||||
tmdbId: number;
|
||||
imdbId?: string;
|
||||
year: number;
|
||||
}
|
||||
|
||||
function SelectMovieRow({ title, year, tmdbId, imdbId }: SelectMovieRowProps) {
|
||||
return (
|
||||
<>
|
||||
<VirtualTableRowCell className={styles.title}>
|
||||
{title}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.year}>{year}</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.imdbId}>
|
||||
{imdbId ? <Label>{imdbId}</Label> : null}
|
||||
</VirtualTableRowCell>
|
||||
|
||||
<VirtualTableRowCell className={styles.tmdbId}>
|
||||
<Label>{tmdbId}</Label>
|
||||
</VirtualTableRowCell>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectMovieRow;
|
||||
@@ -1,240 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageMenuButton from 'Components/Menu/PageMenuButton';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
|
||||
import InteractiveSearchRowConnector from './InteractiveSearchRowConnector';
|
||||
import styles from './InteractiveSearch.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'protocol',
|
||||
label: () => translate('Source'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'age',
|
||||
label: () => translate('Age'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: () => translate('Title'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'indexer',
|
||||
label: () => translate('Indexer'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'history',
|
||||
label: () => translate('History'),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: () => translate('Size'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'peers',
|
||||
label: () => translate('Peers'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Language'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'qualityWeight',
|
||||
label: () => translate('Quality'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.DANGER,
|
||||
title: () => translate('Rejections')
|
||||
}),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'releaseWeight',
|
||||
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
function InteractiveSearch(props) {
|
||||
const {
|
||||
searchPayload,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
totalReleasesCount,
|
||||
items,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
onSortPress,
|
||||
onFilterSelect,
|
||||
onGrabPress
|
||||
} = props;
|
||||
|
||||
const errorMessage = getErrorMessage(error);
|
||||
const type = 'movies';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.filterMenuContainer}>
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
buttonComponent={PageMenuButton}
|
||||
filterModalConnectorComponent={InteractiveSearchFilterModalConnector}
|
||||
filterModalConnectorComponentProps={{ type }}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
isFetching ? <LoadingIndicator /> : null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error ?
|
||||
<Alert kind={kinds.DANGER} className={styles.alert}>
|
||||
{
|
||||
errorMessage ?
|
||||
<Fragment>
|
||||
{translate('InteractiveSearchResultsFailedErrorMessage', { message: errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1) })}
|
||||
</Fragment> :
|
||||
translate('MovieSearchResultsLoadError')
|
||||
}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && isPopulated && !totalReleasesCount ?
|
||||
<Alert kind={kinds.INFO} className={styles.alert}>
|
||||
{translate('NoResultsFound')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!totalReleasesCount && isPopulated && !items.length ?
|
||||
<Alert kind={kinds.WARNING} className={styles.alert}>
|
||||
{translate('AllResultsHiddenFilter')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !!items.length ?
|
||||
<Table
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<InteractiveSearchRowConnector
|
||||
key={`${item.indexerId}-${item.guid}`}
|
||||
{...item}
|
||||
searchPayload={searchPayload}
|
||||
longDateFormat={longDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
onGrabPress={onGrabPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
totalReleasesCount !== items.length && !!items.length ?
|
||||
<Alert kind={kinds.INFO} className={styles.alert}>
|
||||
{translate('SomeResultsHiddenFilter')}
|
||||
</Alert> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
InteractiveSearch.propTypes = {
|
||||
searchPayload: PropTypes.object.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
totalReleasesCount: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default InteractiveSearch;
|
||||
263
frontend/src/InteractiveSearch/InteractiveSearch.tsx
Normal file
263
frontend/src/InteractiveSearch/InteractiveSearch.tsx
Normal file
@@ -0,0 +1,263 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||
import ReleasesAppState from 'App/State/ReleasesAppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageMenuButton from 'Components/Menu/PageMenuButton';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
import { fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
|
||||
import { fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
|
||||
import {
|
||||
fetchReleases,
|
||||
grabRelease,
|
||||
setReleasesFilter,
|
||||
setReleasesSort,
|
||||
} from 'Store/Actions/releaseActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import InteractiveSearchFilterModal from './InteractiveSearchFilterModal';
|
||||
import InteractiveSearchPayload from './InteractiveSearchPayload';
|
||||
import InteractiveSearchRow from './InteractiveSearchRow';
|
||||
import styles from './InteractiveSearch.css';
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
name: 'protocol',
|
||||
label: () => translate('Source'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'age',
|
||||
label: () => translate('Age'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
label: () => translate('Title'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'indexer',
|
||||
label: () => translate('Indexer'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'history',
|
||||
label: () => translate('History'),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
label: () => translate('Size'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'peers',
|
||||
label: () => translate('Peers'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Language'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'qualityWeight',
|
||||
label: () => translate('Quality'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore'),
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags'),
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.DANGER,
|
||||
title: () => translate('Rejections'),
|
||||
}),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'releaseWeight',
|
||||
label: React.createElement(Icon, { name: icons.DOWNLOAD }),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
interface InteractiveSearchProps {
|
||||
searchPayload: InteractiveSearchPayload;
|
||||
}
|
||||
|
||||
function InteractiveSearch({ searchPayload }: InteractiveSearchProps) {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
totalItems,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
}: ReleasesAppState & ClientSideCollectionAppState = useSelector(
|
||||
createClientSideCollectionSelector('releases')
|
||||
);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleFilterSelect = useCallback(
|
||||
(selectedFilterKey: string | number) => {
|
||||
dispatch(setReleasesFilter({ selectedFilterKey }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleSortPress = useCallback(
|
||||
(sortKey: string, sortDirection?: SortDirection) => {
|
||||
dispatch(setReleasesSort({ sortKey, sortDirection }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleGrabPress = useCallback(
|
||||
(payload: object) => {
|
||||
dispatch(grabRelease(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
// Only fetch releases if they are not already being fetched and not yet populated.
|
||||
|
||||
if (!isFetching && !isPopulated) {
|
||||
dispatch(fetchReleases(searchPayload));
|
||||
|
||||
const { movieId } = searchPayload;
|
||||
|
||||
if (movieId) {
|
||||
dispatch(fetchMovieBlocklist({ movieId }));
|
||||
dispatch(fetchMovieHistory({ movieId }));
|
||||
}
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
const errorMessage = getErrorMessage(error);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.filterMenuContainer}>
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
buttonComponent={PageMenuButton}
|
||||
filterModalConnectorComponent={InteractiveSearchFilterModal}
|
||||
filterModalConnectorComponentProps={{ type: 'movies' }}
|
||||
onFilterSelect={handleFilterSelect}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER} className={styles.alert}>
|
||||
{errorMessage ? (
|
||||
<>
|
||||
{translate('InteractiveSearchResultsFailedErrorMessage', {
|
||||
message:
|
||||
errorMessage.charAt(0).toLowerCase() + errorMessage.slice(1),
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
translate('MovieSearchResultsLoadError')
|
||||
)}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{!isFetching && isPopulated && !totalItems ? (
|
||||
<Alert kind={kinds.INFO} className={styles.alert}>
|
||||
{translate('NoResultsFound')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{!!totalItems && isPopulated && !items.length ? (
|
||||
<Alert kind={kinds.WARNING} className={styles.alert}>
|
||||
{translate('AllResultsHiddenFilter')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !!items.length ? (
|
||||
<Table
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={handleSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<InteractiveSearchRow
|
||||
key={`${item.indexerId}-${item.guid}`}
|
||||
{...item}
|
||||
searchPayload={searchPayload}
|
||||
onGrabPress={handleGrabPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
) : null}
|
||||
|
||||
{totalItems !== items.length && !!items.length ? (
|
||||
<Alert kind={kinds.INFO} className={styles.alert}>
|
||||
{translate('SomeResultsHiddenFilter')}
|
||||
</Alert>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default InteractiveSearch;
|
||||
@@ -1,109 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
|
||||
import * as releaseActions from 'Store/Actions/releaseActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import InteractiveSearch from './InteractiveSearch';
|
||||
|
||||
function createMapStateToProps(appState) {
|
||||
return createSelector(
|
||||
(state) => state.releases.items.length,
|
||||
createClientSideCollectionSelector('releases'),
|
||||
createUISettingsSelector(),
|
||||
(totalReleasesCount, releases, uiSettings) => {
|
||||
return {
|
||||
totalReleasesCount,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
...releases
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchFetchReleases(payload) {
|
||||
dispatch(releaseActions.fetchReleases(payload));
|
||||
},
|
||||
|
||||
dispatchFetchMovieHistory({ movieId }) {
|
||||
dispatch(fetchMovieHistory({ movieId }));
|
||||
},
|
||||
|
||||
dispatchClearMovieHistory() {
|
||||
dispatch(clearMovieHistory());
|
||||
},
|
||||
|
||||
onSortPress(sortKey, sortDirection) {
|
||||
dispatch(releaseActions.setReleasesSort({ sortKey, sortDirection }));
|
||||
},
|
||||
|
||||
onFilterSelect(selectedFilterKey) {
|
||||
dispatch(releaseActions.setReleasesFilter({ selectedFilterKey }));
|
||||
},
|
||||
|
||||
onGrabPress(payload) {
|
||||
dispatch(releaseActions.grabRelease(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class InteractiveSearchConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
searchPayload,
|
||||
isPopulated,
|
||||
dispatchFetchReleases,
|
||||
dispatchFetchMovieHistory
|
||||
} = this.props;
|
||||
|
||||
// If search results are not yet isPopulated fetch them,
|
||||
// otherwise re-show the existing props.
|
||||
if (!isPopulated) {
|
||||
dispatchFetchReleases(searchPayload);
|
||||
}
|
||||
|
||||
dispatchFetchMovieHistory(searchPayload);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchClearMovieHistory();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchFetchReleases,
|
||||
dispatchFetchMovieHistory,
|
||||
dispatchClearMovieHistory,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
||||
<InteractiveSearch
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveSearchConnector.propTypes = {
|
||||
searchPayload: PropTypes.object.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
dispatchFetchReleases: PropTypes.func.isRequired,
|
||||
dispatchFetchMovieHistory: PropTypes.func.isRequired,
|
||||
dispatchClearMovieHistory: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchConnector);
|
||||
@@ -0,0 +1,55 @@
|
||||
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 { setReleasesFilter } from 'Store/Actions/releaseActions';
|
||||
|
||||
function createReleasesSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.releases.items,
|
||||
(releases) => {
|
||||
return releases;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.releases.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface InteractiveSearchFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function InteractiveSearchFilterModal({
|
||||
...otherProps
|
||||
}: InteractiveSearchFilterModalProps) {
|
||||
const sectionItems = useSelector(createReleasesSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setReleasesFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...otherProps}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType="releases"
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setReleasesFilter } from 'Store/Actions/releaseActions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.releases.items,
|
||||
(state) => state.releases.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'releases'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchSetFilter(payload) {
|
||||
const action = setReleasesFilter;
|
||||
dispatch(action(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);
|
||||
@@ -0,0 +1,7 @@
|
||||
interface MovieSearchPayload {
|
||||
movieId: number;
|
||||
}
|
||||
|
||||
type InteractiveSearchPayload = MovieSearchPayload;
|
||||
|
||||
export default InteractiveSearchPayload;
|
||||
@@ -1,5 +1,8 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
@@ -8,21 +11,18 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import MovieBlocklist from 'typings/MovieBlocklist';
|
||||
import MovieHistory from 'typings/MovieHistory';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import Release from 'typings/Release';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import InteractiveSearchPayload from './InteractiveSearchPayload';
|
||||
import OverrideMatchModal from './OverrideMatch/OverrideMatchModal';
|
||||
import Peers from './Peers';
|
||||
import styles from './InteractiveSearchRow.css';
|
||||
@@ -71,37 +71,42 @@ function getDownloadTooltip(
|
||||
return translate('AddToDownloadQueue');
|
||||
}
|
||||
|
||||
interface InteractiveSearchRowProps {
|
||||
guid: string;
|
||||
protocol: DownloadProtocol;
|
||||
age: number;
|
||||
ageHours: number;
|
||||
ageMinutes: number;
|
||||
publishDate: string;
|
||||
title: string;
|
||||
infoUrl: string;
|
||||
indexerId: number;
|
||||
indexer: string;
|
||||
size: number;
|
||||
seeders?: number;
|
||||
leechers?: number;
|
||||
quality: QualityModel;
|
||||
languages: Language[];
|
||||
customFormats: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
mappedMovieId?: number;
|
||||
indexerFlags: string[];
|
||||
rejections: string[];
|
||||
downloadAllowed: boolean;
|
||||
isGrabbing: boolean;
|
||||
isGrabbed: boolean;
|
||||
grabError?: string;
|
||||
historyFailedData?: MovieHistory;
|
||||
historyGrabbedData?: MovieHistory;
|
||||
blocklistData?: MovieBlocklist;
|
||||
longDateFormat: string;
|
||||
timeFormat: string;
|
||||
searchPayload: object;
|
||||
function releaseHistorySelector({ guid }: Release) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.movieHistory.items,
|
||||
(state: AppState) => state.movieBlocklist.items,
|
||||
(movieHistory, movieBlocklist) => {
|
||||
let historyFailedData = null;
|
||||
let blocklistedData = null;
|
||||
|
||||
const historyGrabbedData = movieHistory.find(
|
||||
({ eventType, data }) =>
|
||||
eventType === 'grabbed' && 'guid' in data && data.guid === guid
|
||||
);
|
||||
|
||||
if (historyGrabbedData) {
|
||||
historyFailedData = movieHistory.find(
|
||||
({ eventType, sourceTitle }) =>
|
||||
eventType === 'downloadFailed' &&
|
||||
sourceTitle === historyGrabbedData.sourceTitle
|
||||
);
|
||||
|
||||
blocklistedData = movieBlocklist.find(
|
||||
(item) => item.sourceTitle === historyGrabbedData.sourceTitle
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
historyGrabbedData,
|
||||
historyFailedData,
|
||||
blocklistedData,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface InteractiveSearchRowProps extends Release {
|
||||
searchPayload: InteractiveSearchPayload;
|
||||
onGrabPress(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
@@ -130,16 +135,18 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
||||
downloadAllowed,
|
||||
isGrabbing = false,
|
||||
isGrabbed = false,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
grabError,
|
||||
historyGrabbedData = {} as MovieHistory,
|
||||
historyFailedData = {} as MovieHistory,
|
||||
blocklistData = {} as MovieBlocklist,
|
||||
searchPayload,
|
||||
onGrabPress,
|
||||
} = props;
|
||||
|
||||
const { longDateFormat, timeFormat } = useSelector(
|
||||
createUISettingsSelector()
|
||||
);
|
||||
|
||||
const { historyGrabbedData, historyFailedData, blocklistedData } =
|
||||
useSelector(releaseHistorySelector(props));
|
||||
|
||||
const [isConfirmGrabModalOpen, setIsConfirmGrabModalOpen] = useState(false);
|
||||
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
|
||||
|
||||
@@ -211,44 +218,52 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
||||
|
||||
<TableRowCell className={styles.history}>
|
||||
{historyGrabbedData?.date && !historyFailedData?.date ? (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DEFAULT}
|
||||
title={`${translate('Grabbed')}: ${formatDateTime(
|
||||
historyGrabbedData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
)}`}
|
||||
<Tooltip
|
||||
anchor={<Icon name={icons.DOWNLOADING} kind={kinds.DEFAULT} />}
|
||||
tooltip={translate('GrabbedAt', {
|
||||
date: formatDateTime(
|
||||
historyGrabbedData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
),
|
||||
})}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{historyFailedData?.date ? (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title={`${translate('Failed')}: ${formatDateTime(
|
||||
historyFailedData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
)}`}
|
||||
<Tooltip
|
||||
anchor={<Icon name={icons.DOWNLOADING} kind={kinds.DANGER} />}
|
||||
tooltip={translate('FailedAt', {
|
||||
date: formatDateTime(
|
||||
historyFailedData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
),
|
||||
})}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{blocklistData?.date ? (
|
||||
{blocklistedData?.date ? (
|
||||
<Icon
|
||||
className={
|
||||
historyGrabbedData || historyFailedData ? styles.blocklist : ''
|
||||
}
|
||||
name={icons.BLOCKLIST}
|
||||
kind={kinds.DANGER}
|
||||
title={`${translate('Blocklisted')}: ${formatDateTime(
|
||||
blocklistData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
)}`}
|
||||
title={translate('BlocklistedAt', {
|
||||
date: formatDateTime(
|
||||
blocklistedData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
),
|
||||
})}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import InteractiveSearchRow from './InteractiveSearchRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { guid }) => guid,
|
||||
(state) => state.movieHistory.items,
|
||||
(state) => state.movieBlocklist.items,
|
||||
(guid, movieHistory, movieBlocklist) => {
|
||||
|
||||
let blocklistData = {};
|
||||
let historyFailedData = {};
|
||||
|
||||
const historyGrabbedData = movieHistory.find((movie) => movie.eventType === 'grabbed' && movie.data.guid === guid);
|
||||
if (historyGrabbedData) {
|
||||
historyFailedData = movieHistory.find((movie) => movie.eventType === 'downloadFailed' && movie.sourceTitle === historyGrabbedData.sourceTitle);
|
||||
blocklistData = movieBlocklist.find((item) => item.sourceTitle === historyGrabbedData.sourceTitle);
|
||||
}
|
||||
|
||||
return {
|
||||
historyGrabbedData,
|
||||
historyFailedData,
|
||||
blocklistData
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class InteractiveSearchRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
historyGrabbedData,
|
||||
historyFailedData,
|
||||
blocklistData,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<InteractiveSearchRow
|
||||
historyGrabbedData={historyGrabbedData}
|
||||
historyFailedData={historyFailedData}
|
||||
blocklistData={blocklistData}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveSearchRowConnector.propTypes = {
|
||||
historyGrabbedData: PropTypes.object,
|
||||
historyFailedData: PropTypes.object,
|
||||
blocklistData: PropTypes.object
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(InteractiveSearchRowConnector);
|
||||
@@ -1,9 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
||||
function getKind(seeders) {
|
||||
function getKind(seeders: number = 0) {
|
||||
if (seeders > 50) {
|
||||
return kinds.PRIMARY;
|
||||
}
|
||||
@@ -19,7 +18,7 @@ function getKind(seeders) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
function getPeersTooltipPart(peers, peersUnit) {
|
||||
function getPeersTooltipPart(peers: number | undefined, peersUnit: string) {
|
||||
if (peers == null) {
|
||||
return `Unknown ${peersUnit}s`;
|
||||
}
|
||||
@@ -31,27 +30,27 @@ function getPeersTooltipPart(peers, peersUnit) {
|
||||
return `${peers} ${peersUnit}s`;
|
||||
}
|
||||
|
||||
function Peers(props) {
|
||||
const {
|
||||
seeders,
|
||||
leechers
|
||||
} = props;
|
||||
interface PeersProps {
|
||||
seeders?: number;
|
||||
leechers?: number;
|
||||
}
|
||||
|
||||
function Peers(props: PeersProps) {
|
||||
const { seeders, leechers } = props;
|
||||
|
||||
const kind = getKind(seeders);
|
||||
|
||||
return (
|
||||
<Label
|
||||
kind={kind}
|
||||
title={`${getPeersTooltipPart(seeders, 'seeder')}, ${getPeersTooltipPart(leechers, 'leecher')}`}
|
||||
title={`${getPeersTooltipPart(seeders, 'seeder')}, ${getPeersTooltipPart(
|
||||
leechers,
|
||||
'leecher'
|
||||
)}`}
|
||||
>
|
||||
{seeders == null ? '-' : seeders} / {leechers == null ? '-' : leechers}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
Peers.propTypes = {
|
||||
seeders: PropTypes.number,
|
||||
leechers: PropTypes.number
|
||||
};
|
||||
|
||||
export default Peers;
|
||||
@@ -1,37 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import DeleteMovieModalContentConnector from './DeleteMovieModalContentConnector';
|
||||
|
||||
function DeleteMovieModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
previousMovie,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<DeleteMovieModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
previousMovie={previousMovie}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
DeleteMovieModal.propTypes = {
|
||||
...DeleteMovieModalContentConnector.propTypes,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
previousMovie: PropTypes.string
|
||||
};
|
||||
|
||||
export default DeleteMovieModal;
|
||||
24
frontend/src/Movie/Delete/DeleteMovieModal.tsx
Normal file
24
frontend/src/Movie/Delete/DeleteMovieModal.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import DeleteMovieModalContent, {
|
||||
DeleteMovieModalContentProps,
|
||||
} from './DeleteMovieModalContent';
|
||||
|
||||
interface DeleteMovieModalProps extends DeleteMovieModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function DeleteMovieModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: DeleteMovieModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
|
||||
<DeleteMovieModalContent {...otherProps} onModalClose={onModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeleteMovieModal;
|
||||
@@ -1,163 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './DeleteMovieModalContent.css';
|
||||
|
||||
class DeleteMovieModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
deleteFiles: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDeleteFilesChange = ({ value }) => {
|
||||
this.setState({ deleteFiles: value });
|
||||
};
|
||||
|
||||
onDeleteMovieConfirmed = () => {
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
const addImportExclusion = this.props.deleteOptions.addImportExclusion;
|
||||
|
||||
this.setState({ deleteFiles: false });
|
||||
this.props.onDeletePress(deleteFiles, addImportExclusion);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
path,
|
||||
statistics = {},
|
||||
deleteOptions,
|
||||
onModalClose,
|
||||
onDeleteOptionChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
movieFileCount = 0,
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
const addImportExclusion = deleteOptions.addImportExclusion;
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{translate('DeleteHeader', { title })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.pathContainer}>
|
||||
<Icon
|
||||
className={styles.pathIcon}
|
||||
name={icons.FOLDER}
|
||||
/>
|
||||
|
||||
{path}
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('AddListExclusion')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="addImportExclusion"
|
||||
value={addImportExclusion}
|
||||
helpText={translate('AddListExclusionMovieHelpText')}
|
||||
kind={kinds.DANGER}
|
||||
onChange={onDeleteOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{movieFileCount === 0 ? translate('DeleteMovieFolder') : translate('DeleteMovieFiles', { movieFileCount })}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="deleteFiles"
|
||||
value={deleteFiles}
|
||||
helpText={movieFileCount === 0 ? translate('DeleteMovieFolderHelpText') : translate('DeleteMovieFilesHelpText')}
|
||||
kind={kinds.DANGER}
|
||||
onChange={this.onDeleteFilesChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
deleteFiles ?
|
||||
<div className={styles.deleteFilesMessage}>
|
||||
<div><InlineMarkdown data={translate('DeleteMovieFolderConfirmation', { path })} blockClassName={styles.folderPath} /></div>
|
||||
|
||||
{
|
||||
movieFileCount ?
|
||||
<div className={styles.deleteCount}>
|
||||
{translate('DeleteMovieFolderMovieCount', { movieFileCount, size: formatBytes(sizeOnDisk) })}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onDeleteMovieConfirmed}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeleteMovieModalContent.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
hasFile: PropTypes.bool.isRequired,
|
||||
deleteOptions: PropTypes.object.isRequired,
|
||||
onDeleteOptionChange: PropTypes.func.isRequired,
|
||||
onDeletePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
DeleteMovieModalContent.defaultProps = {
|
||||
statistics: {}
|
||||
};
|
||||
|
||||
export default DeleteMovieModalContent;
|
||||
149
frontend/src/Movie/Delete/DeleteMovieModalContent.tsx
Normal file
149
frontend/src/Movie/Delete/DeleteMovieModalContent.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import React, { useCallback, useState } 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 Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||
import { Statistics } from 'Movie/Movie';
|
||||
import useMovie from 'Movie/useMovie';
|
||||
import { deleteMovie, setDeleteOption } from 'Store/Actions/movieActions';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './DeleteMovieModalContent.css';
|
||||
|
||||
export interface DeleteMovieModalContentProps {
|
||||
movieId: number;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function DeleteMovieModalContent({
|
||||
movieId,
|
||||
onModalClose,
|
||||
}: DeleteMovieModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
title,
|
||||
path,
|
||||
collection,
|
||||
statistics = {} as Statistics,
|
||||
} = useMovie(movieId)!;
|
||||
const { addImportExclusion } = useSelector(
|
||||
(state: AppState) => state.movies.deleteOptions
|
||||
);
|
||||
|
||||
const { movieFileCount = 0, sizeOnDisk = 0 } = statistics;
|
||||
|
||||
const [deleteFiles, setDeleteFiles] = useState(false);
|
||||
|
||||
const handleDeleteFilesChange = useCallback(
|
||||
({ value }: CheckInputChanged) => {
|
||||
setDeleteFiles(value);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleDeleteMovieConfirmed = useCallback(() => {
|
||||
dispatch(
|
||||
deleteMovie({
|
||||
id: movieId,
|
||||
collectionTmdbId: collection?.tmdbId,
|
||||
deleteFiles,
|
||||
addImportExclusion,
|
||||
})
|
||||
);
|
||||
}, [movieId, collection, addImportExclusion, deleteFiles, dispatch]);
|
||||
|
||||
const handleDeleteOptionChange = useCallback(
|
||||
({ name, value }: CheckInputChanged) => {
|
||||
dispatch(setDeleteOption({ [name]: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('DeleteHeader', { title })}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.pathContainer}>
|
||||
<Icon className={styles.pathIcon} name={icons.FOLDER} />
|
||||
|
||||
{path}
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AddListExclusion')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="addImportExclusion"
|
||||
value={addImportExclusion}
|
||||
helpText={translate('AddListExclusionMovieHelpText')}
|
||||
kind={kinds.DANGER}
|
||||
onChange={handleDeleteOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{movieFileCount === 0
|
||||
? translate('DeleteMovieFolder')
|
||||
: translate('DeleteMovieFiles', { movieFileCount })}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="deleteFiles"
|
||||
value={deleteFiles}
|
||||
helpText={
|
||||
movieFileCount === 0
|
||||
? translate('DeleteMovieFolderHelpText')
|
||||
: translate('DeleteMovieFilesHelpText')
|
||||
}
|
||||
kind={kinds.DANGER}
|
||||
onChange={handleDeleteFilesChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{deleteFiles ? (
|
||||
<div className={styles.deleteFilesMessage}>
|
||||
<div>
|
||||
<InlineMarkdown
|
||||
data={translate('DeleteMovieFolderConfirmation', { path })}
|
||||
blockClassName={styles.folderPath}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{movieFileCount ? (
|
||||
<div className={styles.deleteCount}>
|
||||
{translate('DeleteMovieFolderMovieCount', {
|
||||
movieFileCount,
|
||||
size: formatBytes(sizeOnDisk),
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
|
||||
<Button kind={kinds.DANGER} onPress={handleDeleteMovieConfirmed}>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default DeleteMovieModalContent;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteMovie, setDeleteOption } from 'Store/Actions/movieActions';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import DeleteMovieModalContent from './DeleteMovieModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movies.deleteOptions,
|
||||
createMovieSelector(),
|
||||
(deleteOptions, movie) => {
|
||||
return {
|
||||
...movie,
|
||||
deleteOptions
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onDeleteOptionChange(option) {
|
||||
dispatch(
|
||||
setDeleteOption({
|
||||
[option.name]: option.value
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
onDeletePress(deleteFiles, addImportExclusion) {
|
||||
dispatch(
|
||||
deleteMovie({
|
||||
id: props.movieId,
|
||||
collectionTmdbId: this.collection?.tmdbId,
|
||||
deleteFiles,
|
||||
addImportExclusion
|
||||
})
|
||||
);
|
||||
|
||||
props.onModalClose(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(DeleteMovieModalContent);
|
||||
@@ -21,19 +21,19 @@ import TmdbRating from 'Components/TmdbRating';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import TraktRating from 'Components/TraktRating';
|
||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, kinds, sizes, sortDirections, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import EditMovieModal from 'Movie/Edit/EditMovieModal';
|
||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
||||
import MovieCollectionLabelConnector from 'Movie/MovieCollectionLabelConnector';
|
||||
import MovieCollectionLabel from 'Movie/MovieCollectionLabel';
|
||||
import MovieGenres from 'Movie/MovieGenres';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
|
||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||
import ExtraFileTable from 'MovieFile/Extras/ExtraFileTable';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
import OrganizePreviewModal from 'Organize/OrganizePreviewModal';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
@@ -609,7 +609,7 @@ class MovieDetails extends Component {
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<div className={styles.collection}>
|
||||
<MovieCollectionLabelConnector
|
||||
<MovieCollectionLabel
|
||||
tmdbId={collection.tmdbId}
|
||||
/>
|
||||
</div>
|
||||
@@ -724,13 +724,13 @@ class MovieDetails extends Component {
|
||||
</FieldSet>
|
||||
</div>
|
||||
|
||||
<OrganizePreviewModalConnector
|
||||
<OrganizePreviewModal
|
||||
isOpen={isOrganizeModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onOrganizeModalClose}
|
||||
/>
|
||||
|
||||
<EditMovieModalConnector
|
||||
<EditMovieModal
|
||||
isOpen={isEditMovieModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onEditMovieModalClose}
|
||||
@@ -747,17 +747,20 @@ class MovieDetails extends Component {
|
||||
isOpen={isDeleteMovieModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onDeleteMovieModalClose}
|
||||
nextMovieRelativePath={`/movie/${nextMovie.titleSlug}`}
|
||||
/>
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
movieId={id}
|
||||
modalTitle={translate('ManageFiles')}
|
||||
title={title}
|
||||
folder={path}
|
||||
initialSortKey="relativePath"
|
||||
initialSortDirection={sortDirections.ASCENDING}
|
||||
showMovie={false}
|
||||
allowMovieChange={false}
|
||||
showFilterExistingFiles={true}
|
||||
showDelete={true}
|
||||
showImportMode={false}
|
||||
modalTitle={translate('ManageFiles')}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
|
||||
|
||||
@@ -8,11 +8,9 @@ import * as commandNames from 'Commands/commandNames';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { clearExtraFiles, fetchExtraFiles } from 'Store/Actions/extraFileActions';
|
||||
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||
import { clearMovieBlocklist, fetchMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
|
||||
import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions';
|
||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||
import { fetchImportListSchema } from 'Store/Actions/settingsActions';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
@@ -188,12 +186,6 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatchClearExtraFiles() {
|
||||
dispatch(clearExtraFiles());
|
||||
},
|
||||
dispatchClearReleases() {
|
||||
dispatch(clearReleases());
|
||||
},
|
||||
dispatchCancelFetchReleases() {
|
||||
dispatch(cancelFetchReleases());
|
||||
},
|
||||
dispatchFetchQueueDetails({ movieId }) {
|
||||
dispatch(fetchQueueDetails({ movieId }));
|
||||
},
|
||||
@@ -211,12 +203,6 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
},
|
||||
onGoToMovie(titleSlug) {
|
||||
dispatch(push(`${window.Radarr.urlBase}/movie/${titleSlug}`));
|
||||
},
|
||||
dispatchFetchMovieBlocklist({ movieId }) {
|
||||
dispatch(fetchMovieBlocklist({ movieId }));
|
||||
},
|
||||
dispatchClearMovieBlocklist() {
|
||||
dispatch(clearMovieBlocklist());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -270,7 +256,6 @@ class MovieDetailsConnector extends Component {
|
||||
const movieId = this.props.id;
|
||||
|
||||
this.props.dispatchFetchMovieFiles({ movieId });
|
||||
this.props.dispatchFetchMovieBlocklist({ movieId });
|
||||
this.props.dispatchFetchExtraFiles({ movieId });
|
||||
this.props.dispatchFetchMovieCredits({ movieId });
|
||||
this.props.dispatchFetchQueueDetails({ movieId });
|
||||
@@ -278,13 +263,10 @@ class MovieDetailsConnector extends Component {
|
||||
};
|
||||
|
||||
unpopulate = () => {
|
||||
this.props.dispatchCancelFetchReleases();
|
||||
this.props.dispatchClearMovieBlocklist();
|
||||
this.props.dispatchClearMovieFiles();
|
||||
this.props.dispatchClearExtraFiles();
|
||||
this.props.dispatchClearMovieCredits();
|
||||
this.props.dispatchClearQueueDetails();
|
||||
this.props.dispatchClearReleases();
|
||||
};
|
||||
|
||||
//
|
||||
@@ -341,15 +323,11 @@ MovieDetailsConnector.propTypes = {
|
||||
dispatchClearExtraFiles: PropTypes.func.isRequired,
|
||||
dispatchFetchMovieCredits: PropTypes.func.isRequired,
|
||||
dispatchClearMovieCredits: PropTypes.func.isRequired,
|
||||
dispatchClearReleases: PropTypes.func.isRequired,
|
||||
dispatchCancelFetchReleases: PropTypes.func.isRequired,
|
||||
dispatchToggleMovieMonitored: PropTypes.func.isRequired,
|
||||
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
||||
dispatchClearQueueDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchImportListSchema: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired,
|
||||
dispatchFetchMovieBlocklist: PropTypes.func.isRequired,
|
||||
dispatchClearMovieBlocklist: PropTypes.func.isRequired,
|
||||
onGoToMovie: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -92,6 +92,19 @@ function MovieDetailsLinks(props: MovieDetailsLinksProps) {
|
||||
MDBList
|
||||
</Label>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`https://www.blu-ray.com/search/?quicksearch=1&quicksearch_keyword=${imdbId}§ion=theatrical`}
|
||||
>
|
||||
<Label
|
||||
className={styles.linkLabel}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
Blu-ray
|
||||
</Label>
|
||||
</Link>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import EditMovieModalContentConnector from './EditMovieModalContentConnector';
|
||||
|
||||
function EditMovieModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditMovieModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditMovieModal.propTypes = {
|
||||
...EditMovieModalContentConnector.propTypes,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditMovieModal;
|
||||
32
frontend/src/Movie/Edit/EditMovieModal.tsx
Normal file
32
frontend/src/Movie/Edit/EditMovieModal.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditMovieModalContent, {
|
||||
EditMovieModalContentProps,
|
||||
} from './EditMovieModalContent';
|
||||
|
||||
interface EditMovieModalProps extends EditMovieModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function EditMovieModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: EditMovieModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
dispatch(clearPendingChanges({ section: 'movies' }));
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
|
||||
<EditMovieModalContent {...otherProps} onModalClose={handleModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditMovieModal;
|
||||
@@ -1,40 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditMovieModal from './EditMovieModal';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class EditMovieModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'movies' });
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditMovieModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditMovieModalConnector.propTypes = {
|
||||
...EditMovieModal.propTypes,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(EditMovieModalConnector);
|
||||
@@ -1,217 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
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 Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditMovieModalContent.css';
|
||||
|
||||
class EditMovieModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isConfirmMoveModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onCancelPress = () => {
|
||||
this.setState({ isConfirmMoveModalOpen: false });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
const {
|
||||
isPathChanging,
|
||||
onSavePress
|
||||
} = this.props;
|
||||
|
||||
if (isPathChanging && !this.state.isConfirmMoveModalOpen) {
|
||||
this.setState({ isConfirmMoveModalOpen: true });
|
||||
} else {
|
||||
this.setState({ isConfirmMoveModalOpen: false });
|
||||
|
||||
onSavePress(false);
|
||||
}
|
||||
};
|
||||
|
||||
onMoveMoviePress = () => {
|
||||
this.setState({ isConfirmMoveModalOpen: false });
|
||||
|
||||
this.props.onSavePress(true);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
item,
|
||||
isSaving,
|
||||
originalPath,
|
||||
onInputChange,
|
||||
onModalClose,
|
||||
onDeleteMoviePress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitored,
|
||||
qualityProfileId,
|
||||
minimumAvailability,
|
||||
// Id,
|
||||
path,
|
||||
tags
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('Edit')} - {title}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form
|
||||
{...otherProps}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Monitored')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="monitored"
|
||||
helpText={translate('MonitoredHelpText')}
|
||||
{...monitored}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('MinimumAvailability')}
|
||||
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={translate('MinimumAvailability')}
|
||||
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
name="minimumAvailability"
|
||||
{...minimumAvailability}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
{...qualityProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Path')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PATH}
|
||||
name="path"
|
||||
{...path}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteMoviePress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerButton
|
||||
isSpinning={isSaving}
|
||||
onPress={this.onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
|
||||
<MoveMovieModal
|
||||
originalPath={originalPath}
|
||||
destinationPath={path.value}
|
||||
isOpen={this.state.isConfirmMoveModalOpen}
|
||||
onModalClose={this.onCancelPress}
|
||||
onSavePress={this.onSavePress}
|
||||
onMoveMoviePress={this.onMoveMoviePress}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditMovieModalContent.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
item: PropTypes.object.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isPathChanging: PropTypes.bool.isRequired,
|
||||
originalPath: PropTypes.string.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onDeleteMoviePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditMovieModalContent;
|
||||
235
frontend/src/Movie/Edit/EditMovieModalContent.tsx
Normal file
235
frontend/src/Movie/Edit/EditMovieModalContent.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
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 Popover from 'Components/Tooltip/Popover';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import {
|
||||
icons,
|
||||
inputTypes,
|
||||
kinds,
|
||||
sizes,
|
||||
tooltipPositions,
|
||||
} from 'Helpers/Props';
|
||||
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
|
||||
import useMovie from 'Movie/useMovie';
|
||||
import { saveMovie, setMovieValue } from 'Store/Actions/movieActions';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import { InputChanged } from 'typings/inputs';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditMovieModalContent.css';
|
||||
|
||||
export interface EditMovieModalContentProps {
|
||||
movieId: number;
|
||||
onModalClose: () => void;
|
||||
onDeleteMoviePress: () => void;
|
||||
}
|
||||
|
||||
function EditMovieModalContent({
|
||||
movieId,
|
||||
onModalClose,
|
||||
onDeleteMoviePress,
|
||||
}: EditMovieModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
title,
|
||||
monitored,
|
||||
minimumAvailability,
|
||||
qualityProfileId,
|
||||
path,
|
||||
tags,
|
||||
} = useMovie(movieId)!;
|
||||
|
||||
const { isSaving, saveError, pendingChanges } = useSelector(
|
||||
(state: AppState) => state.movies
|
||||
);
|
||||
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
const isPathChanging = pendingChanges.path && path !== pendingChanges.path;
|
||||
|
||||
const [isConfirmMoveModalOpen, setIsConfirmMoveModalOpen] = useState(false);
|
||||
|
||||
const { settings, ...otherSettings } = useMemo(() => {
|
||||
return selectSettings(
|
||||
{
|
||||
monitored,
|
||||
minimumAvailability,
|
||||
qualityProfileId,
|
||||
path,
|
||||
tags,
|
||||
},
|
||||
pendingChanges,
|
||||
saveError
|
||||
);
|
||||
}, [
|
||||
monitored,
|
||||
minimumAvailability,
|
||||
qualityProfileId,
|
||||
path,
|
||||
tags,
|
||||
pendingChanges,
|
||||
saveError,
|
||||
]);
|
||||
|
||||
const handleInputChange = useCallback(
|
||||
({ name, value }: InputChanged) => {
|
||||
// @ts-expect-error actions aren't typed
|
||||
dispatch(setMovieValue({ name, value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const handleCancelPress = useCallback(() => {
|
||||
setIsConfirmMoveModalOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleSavePress = useCallback(() => {
|
||||
if (isPathChanging && !isConfirmMoveModalOpen) {
|
||||
setIsConfirmMoveModalOpen(true);
|
||||
} else {
|
||||
setIsConfirmMoveModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
saveMovie({
|
||||
id: movieId,
|
||||
moveFiles: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [movieId, isPathChanging, isConfirmMoveModalOpen, dispatch]);
|
||||
|
||||
const handleMoveMoviePress = useCallback(() => {
|
||||
setIsConfirmMoveModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
saveMovie({
|
||||
id: movieId,
|
||||
moveFiles: true,
|
||||
})
|
||||
);
|
||||
}, [movieId, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaving && wasSaving && !saveError) {
|
||||
onModalClose();
|
||||
}
|
||||
}, [isSaving, wasSaving, saveError, onModalClose]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('EditMovieModalHeader', { title })}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form {...otherSettings}>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('Monitored')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="monitored"
|
||||
helpText={translate('MonitoredMovieHelpText')}
|
||||
{...settings.monitored}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('MinimumAvailability')}
|
||||
|
||||
<Popover
|
||||
anchor={<Icon className={styles.labelIcon} name={icons.INFO} />}
|
||||
title={translate('MinimumAvailability')}
|
||||
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
name="minimumAvailability"
|
||||
{...settings.minimumAvailability}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
{...settings.qualityProfileId}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('Path')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PATH}
|
||||
name="path"
|
||||
{...settings.path}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
{...settings.tags}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
className={styles.deleteButton}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onDeleteMoviePress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</Button>
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
error={saveError}
|
||||
isSpinning={isSaving}
|
||||
onPress={handleSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
|
||||
<MoveMovieModal
|
||||
originalPath={path}
|
||||
destinationPath={pendingChanges.path}
|
||||
isOpen={isConfirmMoveModalOpen}
|
||||
onModalClose={handleCancelPress}
|
||||
onSavePress={handleSavePress}
|
||||
onMoveMoviePress={handleMoveMoviePress}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default EditMovieModalContent;
|
||||
@@ -1,115 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveMovie, setMovieValue } from 'Store/Actions/movieActions';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import EditMovieModalContent from './EditMovieModalContent';
|
||||
|
||||
function createIsPathChangingSelector() {
|
||||
return createSelector(
|
||||
(state) => state.movies.pendingChanges,
|
||||
createMovieSelector(),
|
||||
(pendingChanges, movie) => {
|
||||
const path = pendingChanges.path;
|
||||
|
||||
if (path == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return movie.path !== path;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movies,
|
||||
createMovieSelector(),
|
||||
createIsPathChangingSelector(),
|
||||
(moviesState, movie, isPathChanging) => {
|
||||
const {
|
||||
isSaving,
|
||||
saveError,
|
||||
pendingChanges
|
||||
} = moviesState;
|
||||
|
||||
const movieSettings = {
|
||||
monitored: movie.monitored,
|
||||
qualityProfileId: movie.qualityProfileId,
|
||||
minimumAvailability: movie.minimumAvailability,
|
||||
path: movie.path,
|
||||
tags: movie.tags
|
||||
};
|
||||
|
||||
const settings = selectSettings(movieSettings, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
title: movie.title,
|
||||
isSaving,
|
||||
saveError,
|
||||
isPathChanging,
|
||||
originalPath: movie.path,
|
||||
item: settings.settings,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetMovieValue: setMovieValue,
|
||||
dispatchSaveMovie: saveMovie
|
||||
};
|
||||
|
||||
class EditMovieModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetMovieValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = (moveFiles) => {
|
||||
this.props.dispatchSaveMovie({
|
||||
id: this.props.movieId,
|
||||
moveFiles
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditMovieModalContent
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
onMoveMoviePress={this.onMoveMoviePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditMovieModalContentConnector.propTypes = {
|
||||
movieId: PropTypes.number,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
dispatchSetMovieValue: PropTypes.func.isRequired,
|
||||
dispatchSaveMovie: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditMovieModalContentConnector);
|
||||
@@ -1,33 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import MovieHistoryModalContentConnector from './MovieHistoryModalContentConnector';
|
||||
|
||||
function MovieHistoryModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
>
|
||||
<MovieHistoryModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
MovieHistoryModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieHistoryModal;
|
||||
28
frontend/src/Movie/History/MovieHistoryModal.tsx
Normal file
28
frontend/src/Movie/History/MovieHistoryModal.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import MovieHistoryModalContent, {
|
||||
MovieHistoryModalContentProps,
|
||||
} from 'Movie/History/MovieHistoryModalContent';
|
||||
|
||||
interface MovieHistoryModalProps extends MovieHistoryModalContentProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
function MovieHistoryModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: MovieHistoryModalProps) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_EXTRA_LARGE}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<MovieHistoryModalContent {...otherProps} onModalClose={onModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieHistoryModal;
|
||||
@@ -1,141 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieHistoryRowConnector from './MovieHistoryRowConnector';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'eventType',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: () => translate('SourceTitle'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('CustomFormats'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: () => translate('Date'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class MovieHistoryModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
onMarkAsFailedPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const hasItems = !!items.length;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('History')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !hasItems && !error &&
|
||||
<div>{translate('NoHistory')}</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && hasItems && !error &&
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<MovieHistoryRowConnector
|
||||
key={item.id}
|
||||
{...item}
|
||||
onMarkAsFailedPress={onMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieHistoryModalContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieHistoryModalContent;
|
||||
148
frontend/src/Movie/History/MovieHistoryModalContent.tsx
Normal file
148
frontend/src/Movie/History/MovieHistoryModalContent.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import Column from 'Components/Table/Column';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
clearMovieHistory,
|
||||
fetchMovieHistory,
|
||||
movieHistoryMarkAsFailed,
|
||||
} from 'Store/Actions/movieHistoryActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieHistoryRow from './MovieHistoryRow';
|
||||
|
||||
const columns: Column[] = [
|
||||
{
|
||||
name: 'eventType',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: () => translate('SourceTitle'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: () => translate('Languages'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: () => translate('Quality'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('CustomFormats'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore'),
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: () => translate('Date'),
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: '',
|
||||
isVisible: true,
|
||||
},
|
||||
];
|
||||
|
||||
export interface MovieHistoryModalContentProps {
|
||||
movieId: number;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function MovieHistoryModalContent({
|
||||
movieId,
|
||||
onModalClose,
|
||||
}: MovieHistoryModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { isFetching, isPopulated, error, items } = useSelector(
|
||||
(state: AppState) => state.movieHistory
|
||||
);
|
||||
|
||||
const hasItems = !!items.length;
|
||||
|
||||
const handleMarkAsFailedPress = useCallback(
|
||||
(historyId: number) => {
|
||||
dispatch(
|
||||
movieHistoryMarkAsFailed({
|
||||
historyId,
|
||||
movieId,
|
||||
})
|
||||
);
|
||||
},
|
||||
[movieId, dispatch]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchMovieHistory({ movieId }));
|
||||
|
||||
return () => {
|
||||
dispatch(clearMovieHistory());
|
||||
};
|
||||
}, [movieId, dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('History')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !hasItems && !error ? (
|
||||
<div>{translate('NoHistory')}</div>
|
||||
) : null}
|
||||
|
||||
{isPopulated && hasItems && !error && (
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<MovieHistoryRow
|
||||
key={item.id}
|
||||
{...item}
|
||||
onMarkAsFailedPress={handleMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieHistoryModalContent;
|
||||
@@ -1,76 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearMovieHistory, fetchMovieHistory, movieHistoryMarkAsFailed } from 'Store/Actions/movieHistoryActions';
|
||||
import MovieHistoryModalContent from './MovieHistoryModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movieHistory,
|
||||
(movieHistory) => {
|
||||
return movieHistory;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchMovieHistory,
|
||||
clearMovieHistory,
|
||||
movieHistoryMarkAsFailed
|
||||
};
|
||||
|
||||
class MovieHistoryModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
movieId
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchMovieHistory({
|
||||
movieId
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearMovieHistory();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMarkAsFailedPress = (historyId) => {
|
||||
const {
|
||||
movieId
|
||||
} = this.props;
|
||||
|
||||
this.props.movieHistoryMarkAsFailed({
|
||||
historyId,
|
||||
movieId
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MovieHistoryModalContent
|
||||
{...this.props}
|
||||
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieHistoryModalContentConnector.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
fetchMovieHistory: PropTypes.func.isRequired,
|
||||
clearMovieHistory: PropTypes.func.isRequired,
|
||||
movieHistoryMarkAsFailed: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryModalContentConnector);
|
||||
@@ -1,181 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import HistoryDetailsModal from 'Activity/History/Details/HistoryDetailsModal';
|
||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
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 formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MovieHistoryRow.css';
|
||||
|
||||
class MovieHistoryRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isMarkAsFailedModalOpen: false,
|
||||
isDetailsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMarkAsFailedPress = () => {
|
||||
this.setState({ isMarkAsFailedModalOpen: true });
|
||||
};
|
||||
|
||||
onConfirmMarkAsFailed = () => {
|
||||
this.props.onMarkAsFailedPress(this.props.id);
|
||||
this.setState({ isMarkAsFailedModalOpen: false });
|
||||
};
|
||||
|
||||
onMarkAsFailedModalClose = () => {
|
||||
this.setState({ isMarkAsFailedModalOpen: false });
|
||||
};
|
||||
|
||||
onDetailsPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
};
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
eventType,
|
||||
sourceTitle,
|
||||
quality,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
languages,
|
||||
qualityCutoffNotMet,
|
||||
date,
|
||||
data,
|
||||
downloadId,
|
||||
isMarkingAsFailed,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isMarkAsFailedModalOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<HistoryEventTypeCell
|
||||
eventType={eventType}
|
||||
data={data}
|
||||
/>
|
||||
|
||||
<TableRowCell className={styles.sourceTitle}>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<MovieLanguages
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
isCutoffNotMet={qualityCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<MovieFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormatScore}>
|
||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCell
|
||||
date={date}
|
||||
includeSeconds={true}
|
||||
includeTime={true}
|
||||
/>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
/>
|
||||
|
||||
{
|
||||
eventType === 'grabbed' &&
|
||||
<IconButton
|
||||
title={translate('MarkAsFailed')}
|
||||
name={icons.REMOVE}
|
||||
size={14}
|
||||
onPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isMarkAsFailedModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('MarkAsFailed')}
|
||||
message={translate('MarkAsFailedMessageText', [sourceTitle])}
|
||||
confirmLabel={translate('MarkAsFailed')}
|
||||
onConfirm={this.onConfirmMarkAsFailed}
|
||||
onCancel={this.onMarkAsFailedModalClose}
|
||||
/>
|
||||
|
||||
<HistoryDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
eventType={eventType}
|
||||
sourceTitle={sourceTitle}
|
||||
data={data}
|
||||
downloadId={downloadId}
|
||||
isMarkingAsFailed={isMarkingAsFailed}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieHistoryRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
downloadId: PropTypes.string,
|
||||
isMarkingAsFailed: PropTypes.bool,
|
||||
movie: PropTypes.object.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieHistoryRow;
|
||||
134
frontend/src/Movie/History/MovieHistoryRow.tsx
Normal file
134
frontend/src/Movie/History/MovieHistoryRow.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import HistoryDetailsModal from 'Activity/History/Details/HistoryDetailsModal';
|
||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguages from 'Movie/MovieLanguages';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import { HistoryData, HistoryEventType } from 'typings/History';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MovieHistoryRow.css';
|
||||
|
||||
interface MovieHistoryRowProps {
|
||||
id: number;
|
||||
eventType: HistoryEventType;
|
||||
sourceTitle: string;
|
||||
languages?: Language[];
|
||||
quality: QualityModel;
|
||||
qualityCutoffNotMet: boolean;
|
||||
customFormats?: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
date: string;
|
||||
data: HistoryData;
|
||||
downloadId?: string;
|
||||
onMarkAsFailedPress: (historyId: number) => void;
|
||||
}
|
||||
|
||||
function MovieHistoryRow({
|
||||
id,
|
||||
eventType,
|
||||
sourceTitle,
|
||||
languages = [],
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
customFormats = [],
|
||||
customFormatScore,
|
||||
date,
|
||||
data,
|
||||
downloadId,
|
||||
onMarkAsFailedPress,
|
||||
}: MovieHistoryRowProps) {
|
||||
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||
const [isMarkAsFailedModalOpen, setIsMarkAsFailedModalOpen] = useState(false);
|
||||
|
||||
const handleDetailsPress = useCallback(() => {
|
||||
setIsDetailsModalOpen(true);
|
||||
}, [setIsDetailsModalOpen]);
|
||||
|
||||
const handleDetailsModalClose = useCallback(() => {
|
||||
setIsDetailsModalOpen(false);
|
||||
}, [setIsDetailsModalOpen]);
|
||||
|
||||
const handleMarkAsFailedPress = useCallback(() => {
|
||||
setIsMarkAsFailedModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleConfirmMarkAsFailed = useCallback(() => {
|
||||
onMarkAsFailedPress(id);
|
||||
setIsMarkAsFailedModalOpen(false);
|
||||
}, [id, onMarkAsFailedPress]);
|
||||
|
||||
const handleMarkAsFailedModalClose = useCallback(() => {
|
||||
setIsMarkAsFailedModalOpen(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<HistoryEventTypeCell eventType={eventType} data={data} />
|
||||
|
||||
<TableRowCell className={styles.sourceTitle}>{sourceTitle}</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<MovieLanguages languages={languages} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<MovieQuality quality={quality} isCutoffNotMet={qualityCutoffNotMet} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<MovieFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormatScore}>
|
||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCell date={date} includeSeconds={true} includeTime={true} />
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
|
||||
|
||||
{eventType === 'grabbed' ? (
|
||||
<IconButton
|
||||
title={translate('MarkAsFailed')}
|
||||
name={icons.REMOVE}
|
||||
size={14}
|
||||
onPress={handleMarkAsFailedPress}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isMarkAsFailedModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('MarkAsFailed')}
|
||||
message={translate('MarkAsFailedConfirmation', { sourceTitle })}
|
||||
confirmLabel={translate('MarkAsFailed')}
|
||||
onConfirm={handleConfirmMarkAsFailed}
|
||||
onCancel={handleMarkAsFailedModalClose}
|
||||
/>
|
||||
|
||||
<HistoryDetailsModal
|
||||
isOpen={isDetailsModalOpen}
|
||||
eventType={eventType}
|
||||
sourceTitle={sourceTitle}
|
||||
data={data}
|
||||
downloadId={downloadId}
|
||||
onMarkAsFailedPress={handleMarkAsFailedPress}
|
||||
onModalClose={handleDetailsModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieHistoryRow;
|
||||
@@ -1,27 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import MovieHistoryRow from './MovieHistoryRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieSelector(),
|
||||
createUISettingsSelector(),
|
||||
(movie, uiSettings) => {
|
||||
return {
|
||||
movie,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchHistory,
|
||||
markAsFailed
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieHistoryRow);
|
||||
@@ -11,7 +11,7 @@ import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import EditMovieModal from 'Movie/Edit/EditMovieModal';
|
||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
|
||||
import { Statistics } from 'Movie/Movie';
|
||||
@@ -250,7 +250,7 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditMovieModalConnector
|
||||
<EditMovieModal
|
||||
isOpen={isEditMovieModalOpen}
|
||||
movieId={movieId}
|
||||
onModalClose={onEditMovieModalClose}
|
||||
|
||||
@@ -15,7 +15,7 @@ import TraktRating from 'Components/TraktRating';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import EditMovieModal from 'Movie/Edit/EditMovieModal';
|
||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
|
||||
import { Statistics } from 'Movie/Movie';
|
||||
@@ -384,7 +384,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||
showTags={showTags}
|
||||
/>
|
||||
|
||||
<EditMovieModalConnector
|
||||
<EditMovieModal
|
||||
isOpen={isEditMovieModalOpen}
|
||||
movieId={movieId}
|
||||
onModalClose={onEditMovieModalClose}
|
||||
|
||||
@@ -18,7 +18,7 @@ import TraktRating from 'Components/TraktRating';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import EditMovieModal from 'Movie/Edit/EditMovieModal';
|
||||
import createMovieIndexItemSelector from 'Movie/Index/createMovieIndexItemSelector';
|
||||
import { Statistics } from 'Movie/Movie';
|
||||
import MoviePopularityIndex from 'Movie/MoviePopularityIndex';
|
||||
@@ -480,7 +480,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
||||
return null;
|
||||
})}
|
||||
|
||||
<EditMovieModalConnector
|
||||
<EditMovieModal
|
||||
isOpen={isEditMovieModalOpen}
|
||||
movieId={movieId}
|
||||
onModalClose={onEditMovieModalClose}
|
||||
|
||||
@@ -3,3 +3,7 @@
|
||||
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.folderRenameMessage {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'doNotMoveButton': string;
|
||||
'folderRenameMessage': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
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 { kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MoveMovieModal.css';
|
||||
|
||||
function MoveMovieModal(props) {
|
||||
const {
|
||||
originalPath,
|
||||
destinationPath,
|
||||
destinationRootFolder,
|
||||
isOpen,
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onMoveMoviePress
|
||||
} = props;
|
||||
|
||||
if (
|
||||
isOpen &&
|
||||
!originalPath &&
|
||||
!destinationPath &&
|
||||
!destinationRootFolder
|
||||
) {
|
||||
console.error('orginalPath and destinationPath OR destinationRootFolder must be provided');
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
showCloseButton={true}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{translate('MoveFiles')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
destinationRootFolder ?
|
||||
translate('MoveFolders1', [destinationRootFolder]) :
|
||||
translate('MoveFolders2', [originalPath, destinationPath])
|
||||
}
|
||||
{
|
||||
destinationRootFolder ?
|
||||
<div>
|
||||
{translate('FolderMoveRenameWarning')}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
className={styles.doNotMoveButton}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
{translate('NoMoveFilesSelf')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={onMoveMoviePress}
|
||||
>
|
||||
{translate('YesMoveFiles')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
MoveMovieModal.propTypes = {
|
||||
originalPath: PropTypes.string,
|
||||
destinationPath: PropTypes.string,
|
||||
destinationRootFolder: PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onMoveMoviePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MoveMovieModal;
|
||||
82
frontend/src/Movie/MoveMovie/MoveMovieModal.tsx
Normal file
82
frontend/src/Movie/MoveMovie/MoveMovieModal.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
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 { kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MoveMovieModal.css';
|
||||
|
||||
interface MoveMovieModalProps {
|
||||
originalPath?: string;
|
||||
destinationPath?: string;
|
||||
destinationRootFolder?: string;
|
||||
isOpen: boolean;
|
||||
onModalClose: () => void;
|
||||
onSavePress: () => void;
|
||||
onMoveMoviePress: () => void;
|
||||
}
|
||||
|
||||
function MoveMovieModal({
|
||||
originalPath,
|
||||
destinationPath,
|
||||
destinationRootFolder,
|
||||
isOpen,
|
||||
onModalClose,
|
||||
onSavePress,
|
||||
onMoveMoviePress,
|
||||
}: MoveMovieModalProps) {
|
||||
if (isOpen && !originalPath && !destinationPath && !destinationRootFolder) {
|
||||
console.error(
|
||||
'originalPath and destinationPath OR destinationRootFolder must be provided'
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalContent showCloseButton={true} onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('MoveFiles')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{destinationRootFolder
|
||||
? translate('MoveMovieFoldersToRootFolder', {
|
||||
destinationRootFolder,
|
||||
})
|
||||
: null}
|
||||
|
||||
{originalPath && destinationPath
|
||||
? translate('MoveMovieFoldersToNewPath', {
|
||||
originalPath,
|
||||
destinationPath,
|
||||
})
|
||||
: null}
|
||||
|
||||
{destinationRootFolder ? (
|
||||
<div className={styles.folderRenameMessage}>
|
||||
{translate('MoveMovieFoldersRenameFolderWarning')}
|
||||
</div>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button className={styles.doNotMoveButton} onPress={onSavePress}>
|
||||
{translate('MoveMovieFoldersDontMoveFiles')}
|
||||
</Button>
|
||||
|
||||
<Button kind={kinds.DANGER} onPress={onMoveMoviePress}>
|
||||
{translate('MoveMovieFoldersMoveFiles')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default MoveMovieModal;
|
||||
@@ -18,6 +18,7 @@ export interface Image {
|
||||
}
|
||||
|
||||
export interface Collection {
|
||||
tmdbId: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
@@ -74,11 +75,12 @@ interface Movie extends ModelBase {
|
||||
ratings: Ratings;
|
||||
popularity: number;
|
||||
certification: string;
|
||||
statistics: Statistics;
|
||||
statistics?: Statistics;
|
||||
tags: number[];
|
||||
images: Image[];
|
||||
movieFile: MovieFile;
|
||||
hasFile: boolean;
|
||||
grabbed?: boolean;
|
||||
lastSearchTime?: string;
|
||||
isAvailable: boolean;
|
||||
isSaving?: boolean;
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import styles from './MovieCollectionLabel.css';
|
||||
|
||||
class MovieCollectionLabel extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
hasPosterError: false
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
monitored,
|
||||
onMonitorTogglePress
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
size={15}
|
||||
onPress={onMonitorTogglePress}
|
||||
/>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieCollectionLabel.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
onMonitorTogglePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MovieCollectionLabel;
|
||||
46
frontend/src/Movie/MovieCollectionLabel.tsx
Normal file
46
frontend/src/Movie/MovieCollectionLabel.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import { toggleCollectionMonitored } from 'Store/Actions/movieCollectionActions';
|
||||
import { createCollectionSelectorForHook } from 'Store/Selectors/createCollectionSelector';
|
||||
import MovieCollection from 'typings/MovieCollection';
|
||||
import styles from './MovieCollectionLabel.css';
|
||||
|
||||
interface MovieCollectionLabelProps {
|
||||
tmdbId: number;
|
||||
}
|
||||
|
||||
function MovieCollectionLabel({ tmdbId }: MovieCollectionLabelProps) {
|
||||
const {
|
||||
id,
|
||||
monitored,
|
||||
title,
|
||||
isSaving = false,
|
||||
} = useSelector(createCollectionSelectorForHook(tmdbId)) as MovieCollection;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleMonitorTogglePress = useCallback(
|
||||
(value: boolean) => {
|
||||
dispatch(
|
||||
toggleCollectionMonitored({ collectionId: id, monitored: value })
|
||||
);
|
||||
},
|
||||
[id, dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
isSaving={isSaving}
|
||||
size={15}
|
||||
onPress={handleMonitorTogglePress}
|
||||
/>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieCollectionLabel;
|
||||
@@ -1,57 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { toggleCollectionMonitored } from 'Store/Actions/movieCollectionActions';
|
||||
import MovieCollectionLabel from './MovieCollectionLabel';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { tmdbId }) => tmdbId,
|
||||
(state) => state.movieCollections.items,
|
||||
(tmdbId, collections) => {
|
||||
const collection = collections.find((movie) => movie.tmdbId === tmdbId);
|
||||
return {
|
||||
...collection
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleCollectionMonitored
|
||||
};
|
||||
|
||||
class MovieCollectionLabelConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMonitorTogglePress = (monitored) => {
|
||||
this.props.toggleCollectionMonitored({
|
||||
collectionId: this.props.id,
|
||||
monitored
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MovieCollectionLabel
|
||||
{...this.props}
|
||||
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieCollectionLabelConnector.propTypes = {
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
id: PropTypes.number.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
toggleCollectionMonitored: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCollectionLabelConnector);
|
||||
@@ -1,33 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
||||
function MovieFormats({ formats }) {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
formats.map((format) => {
|
||||
return (
|
||||
<Label
|
||||
key={format.id}
|
||||
kind={kinds.INFO}
|
||||
>
|
||||
{format.name}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MovieFormats.propTypes = {
|
||||
formats: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
MovieFormats.defaultProps = {
|
||||
formats: []
|
||||
};
|
||||
|
||||
export default MovieFormats;
|
||||
22
frontend/src/Movie/MovieFormats.tsx
Normal file
22
frontend/src/Movie/MovieFormats.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
|
||||
interface MovieFormatsProps {
|
||||
formats: CustomFormat[];
|
||||
}
|
||||
|
||||
function MovieFormats({ formats }: MovieFormatsProps) {
|
||||
return (
|
||||
<div>
|
||||
{formats.map(({ id, name }) => (
|
||||
<Label key={id} kind={kinds.INFO}>
|
||||
{name}
|
||||
</Label>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MovieFormats;
|
||||
@@ -1,32 +1,40 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import QueueDetails from 'Activity/Queue/QueueDetails';
|
||||
import Icon from 'Components/Icon';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import Movie from 'Movie/Movie';
|
||||
import useMovie, { MovieEntity } from 'Movie/useMovie';
|
||||
import useMovieFile from 'MovieFile/useMovieFile';
|
||||
import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import MovieQuality from './MovieQuality';
|
||||
import styles from './MovieStatus.css';
|
||||
|
||||
function MovieStatus(props) {
|
||||
interface MovieStatusProps {
|
||||
movieId: number;
|
||||
movieEntity?: MovieEntity;
|
||||
movieFileId: number | undefined;
|
||||
}
|
||||
|
||||
function MovieStatus({ movieId, movieFileId }: MovieStatusProps) {
|
||||
const {
|
||||
isAvailable,
|
||||
monitored,
|
||||
grabbed,
|
||||
queueItem,
|
||||
movieFile
|
||||
} = props;
|
||||
grabbed = false,
|
||||
} = useMovie(movieId) as Movie;
|
||||
|
||||
const queueItem = useSelector(createQueueItemSelectorForHook(movieId));
|
||||
const movieFile = useMovieFile(movieFileId);
|
||||
|
||||
const hasMovieFile = !!movieFile;
|
||||
const isQueued = !!queueItem;
|
||||
|
||||
if (isQueued) {
|
||||
const {
|
||||
sizeleft,
|
||||
size
|
||||
} = queueItem;
|
||||
const { sizeleft, size } = queueItem;
|
||||
|
||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
|
||||
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
@@ -86,30 +94,16 @@ function MovieStatus(props) {
|
||||
if (isAvailable) {
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.MISSING}
|
||||
title={translate('MovieMissingFromDisk')}
|
||||
/>
|
||||
<Icon name={icons.MISSING} title={translate('MovieMissingFromDisk')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<Icon
|
||||
name={icons.NOT_AIRED}
|
||||
title={translate('MovieIsNotAvailable')}
|
||||
/>
|
||||
<Icon name={icons.NOT_AIRED} title={translate('MovieIsNotAvailable')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
MovieStatus.propTypes = {
|
||||
isAvailable: PropTypes.bool.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
grabbed: PropTypes.bool,
|
||||
queueItem: PropTypes.object,
|
||||
movieFile: PropTypes.object
|
||||
};
|
||||
|
||||
export default MovieStatus;
|
||||
@@ -1,50 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import MovieStatus from 'Movie/MovieStatus';
|
||||
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
|
||||
import { createMovieByEntitySelector } from 'Store/Selectors/createMovieSelector';
|
||||
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieByEntitySelector(),
|
||||
createQueueItemSelector(),
|
||||
createMovieFileSelector(),
|
||||
(movie, queueItem, movieFile) => {
|
||||
const result = _.pick(movie, [
|
||||
'isAvailable',
|
||||
'monitored',
|
||||
'grabbed'
|
||||
]);
|
||||
|
||||
result.queueItem = queueItem;
|
||||
result.movieFile = movieFile;
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class MovieStatusConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MovieStatus
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MovieStatusConnector.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
movieFileId: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, null)(MovieStatusConnector);
|
||||
@@ -2,6 +2,8 @@ import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import { clearMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
|
||||
import { clearMovieHistory } from 'Store/Actions/movieHistoryActions';
|
||||
import {
|
||||
cancelFetchReleases,
|
||||
clearReleases,
|
||||
@@ -24,6 +26,9 @@ function MovieInteractiveSearchModal(props: MovieInteractiveSearchModalProps) {
|
||||
dispatch(cancelFetchReleases());
|
||||
dispatch(clearReleases());
|
||||
|
||||
dispatch(clearMovieBlocklist());
|
||||
dispatch(clearMovieHistory());
|
||||
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
||||
import InteractiveSearch from 'InteractiveSearch/InteractiveSearch';
|
||||
import { clearMovieBlocklist } from 'Store/Actions/movieBlocklistActions';
|
||||
import { clearMovieHistory } from 'Store/Actions/movieHistoryActions';
|
||||
import {
|
||||
cancelFetchReleases,
|
||||
clearReleases,
|
||||
@@ -30,6 +32,9 @@ function MovieInteractiveSearchModalContent(
|
||||
return () => {
|
||||
dispatch(cancelFetchReleases());
|
||||
dispatch(clearReleases());
|
||||
|
||||
dispatch(clearMovieBlocklist());
|
||||
dispatch(clearMovieHistory());
|
||||
};
|
||||
}, [dispatch]);
|
||||
|
||||
@@ -44,7 +49,7 @@ function MovieInteractiveSearchModalContent(
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody scrollDirection={scrollDirections.BOTH}>
|
||||
<InteractiveSearchConnector searchPayload={{ movieId }} />
|
||||
<InteractiveSearch searchPayload={{ movieId }} />
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
|
||||
@@ -2,6 +2,13 @@ import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
export type MovieEntity =
|
||||
| 'calendar'
|
||||
| 'movies'
|
||||
| 'interactiveImport.movies'
|
||||
| 'wanted.cutoffUnmet'
|
||||
| 'wanted.missing';
|
||||
|
||||
export function createMovieSelector(movieId?: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.movies.itemMap,
|
||||
@@ -12,7 +19,7 @@ export function createMovieSelector(movieId?: number) {
|
||||
);
|
||||
}
|
||||
|
||||
function useMovie(movieId?: number) {
|
||||
function useMovie(movieId: number | undefined) {
|
||||
return useSelector(createMovieSelector(movieId));
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import MediaInfoProps from 'typings/MediaInfo';
|
||||
import formatBitrate from 'Utilities/Number/formatBitrate';
|
||||
import getEntries from 'Utilities/Object/getEntries';
|
||||
|
||||
function MediaInfo(props: MediaInfoProps) {
|
||||
@@ -16,9 +17,19 @@ function MediaInfo(props: MediaInfoProps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<DescriptionListItem key={key} title={title} data={props[key]} />
|
||||
);
|
||||
if (key === 'audioBitrate' || key === 'videoBitrate') {
|
||||
return (
|
||||
<DescriptionListItem
|
||||
key={key}
|
||||
title={title}
|
||||
data={
|
||||
<span title={value.toString()}>{formatBitrate(value)}</span>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <DescriptionListItem key={key} title={title} data={value} />;
|
||||
})}
|
||||
</DescriptionList>
|
||||
);
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import OrganizePreviewModalContentConnector from './OrganizePreviewModalContentConnector';
|
||||
|
||||
function OrganizePreviewModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
{
|
||||
isOpen &&
|
||||
<OrganizePreviewModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
OrganizePreviewModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default OrganizePreviewModal;
|
||||
37
frontend/src/Organize/OrganizePreviewModal.tsx
Normal file
37
frontend/src/Organize/OrganizePreviewModal.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { clearOrganizePreview } from 'Store/Actions/organizePreviewActions';
|
||||
import OrganizePreviewModalContent, {
|
||||
OrganizePreviewModalContentProps,
|
||||
} from './OrganizePreviewModalContent';
|
||||
|
||||
interface OrganizePreviewModalProps extends OrganizePreviewModalContentProps {
|
||||
isOpen: boolean;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function OrganizePreviewModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: OrganizePreviewModalProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleOnModalClose = useCallback(() => {
|
||||
dispatch(clearOrganizePreview());
|
||||
onModalClose();
|
||||
}, [dispatch, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={handleOnModalClose}>
|
||||
{isOpen ? (
|
||||
<OrganizePreviewModalContent
|
||||
{...otherProps}
|
||||
onModalClose={handleOnModalClose}
|
||||
/>
|
||||
) : null}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
export default OrganizePreviewModal;
|
||||
@@ -1,39 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearOrganizePreview } from 'Store/Actions/organizePreviewActions';
|
||||
import OrganizePreviewModal from './OrganizePreviewModal';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearOrganizePreview
|
||||
};
|
||||
|
||||
class OrganizePreviewModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearOrganizePreview();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<OrganizePreviewModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OrganizePreviewModalConnector.propTypes = {
|
||||
clearOrganizePreview: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(OrganizePreviewModalConnector);
|
||||
@@ -1,196 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
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 { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import OrganizePreviewRow from './OrganizePreviewRow';
|
||||
import styles from './OrganizePreviewModalContent.css';
|
||||
|
||||
function getValue(allSelected, allUnselected) {
|
||||
if (allSelected) {
|
||||
return true;
|
||||
} else if (allUnselected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
class OrganizePreviewModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {}
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// 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);
|
||||
});
|
||||
};
|
||||
|
||||
onOrganizePress = () => {
|
||||
this.props.onOrganizePress(this.getSelectedIds());
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
renameMovies,
|
||||
standardMovieFormat,
|
||||
path,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const selectAllValue = getValue(allSelected, allUnselected);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('OrganizeModalHeader')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<Alert kind={kinds.DANGER}>{translate('OrganizeLoadError')}</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && isPopulated && !items.length &&
|
||||
<div>
|
||||
{
|
||||
renameMovies ?
|
||||
<div>{translate('OrganizeNothingToRename')}</div> :
|
||||
<div>{translate('OrganizeRenamingDisabled')}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && isPopulated && !!items.length &&
|
||||
<div>
|
||||
<Alert>
|
||||
<div>
|
||||
<InlineMarkdown data={translate('OrganizeRelativePaths', { path })} blockClassName={styles.path} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InlineMarkdown data={translate('OrganizeNamingPattern', { standardMovieFormat })} blockClassName={styles.standardMovieFormat} />
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<div className={styles.previews}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<OrganizePreviewRow
|
||||
key={item.movieFileId}
|
||||
id={item.movieFileId}
|
||||
existingPath={item.existingPath}
|
||||
newPath={item.newPath}
|
||||
isSelected={selectedState[item.movieFileId]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{
|
||||
isPopulated && !!items.length &&
|
||||
<CheckInput
|
||||
className={styles.selectAllInput}
|
||||
containerClassName={styles.selectAllInputContainer}
|
||||
name="selectAll"
|
||||
value={selectAllValue}
|
||||
onChange={this.onSelectAllChange}
|
||||
/>
|
||||
}
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
onPress={this.onOrganizePress}
|
||||
>
|
||||
{translate('Organize')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OrganizePreviewModalContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
renameMovies: PropTypes.bool,
|
||||
standardMovieFormat: PropTypes.string,
|
||||
onOrganizePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default OrganizePreviewModalContent;
|
||||
193
frontend/src/Organize/OrganizePreviewModalContent.tsx
Normal file
193
frontend/src/Organize/OrganizePreviewModalContent.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
import React, { useCallback, useEffect } 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 CheckInput from 'Components/Form/CheckInput';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
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 useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import useMovie from 'Movie/useMovie';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchOrganizePreview } from 'Store/Actions/organizePreviewActions';
|
||||
import { fetchNamingSettings } from 'Store/Actions/settingsActions';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import OrganizePreviewRow from './OrganizePreviewRow';
|
||||
import styles from './OrganizePreviewModalContent.css';
|
||||
|
||||
function getValue(allSelected: boolean, allUnselected: boolean) {
|
||||
if (allSelected) {
|
||||
return true;
|
||||
} else if (allUnselected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface OrganizePreviewModalContentProps {
|
||||
movieId: number;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
function OrganizePreviewModalContent({
|
||||
movieId,
|
||||
onModalClose,
|
||||
}: OrganizePreviewModalContentProps) {
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
items,
|
||||
isFetching: isPreviewFetching,
|
||||
isPopulated: isPreviewPopulated,
|
||||
error: previewError,
|
||||
} = useSelector((state: AppState) => state.organizePreview);
|
||||
|
||||
const {
|
||||
isFetching: isNamingFetching,
|
||||
isPopulated: isNamingPopulated,
|
||||
error: namingError,
|
||||
item: naming,
|
||||
} = useSelector((state: AppState) => state.settings.naming);
|
||||
|
||||
const movie = useMovie(movieId)!;
|
||||
const [selectState, setSelectState] = useSelectState();
|
||||
|
||||
const { allSelected, allUnselected, selectedState } = selectState;
|
||||
const isFetching = isPreviewFetching || isNamingFetching;
|
||||
const isPopulated = isPreviewPopulated && isNamingPopulated;
|
||||
const error = previewError || namingError;
|
||||
const { renameMovies, standardMovieFormat } = naming;
|
||||
|
||||
const selectAllValue = getValue(allSelected, allUnselected);
|
||||
|
||||
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 handleOrganizePress = useCallback(() => {
|
||||
const files = getSelectedIds(selectedState);
|
||||
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: commandNames.RENAME_FILES,
|
||||
files,
|
||||
movieId,
|
||||
})
|
||||
);
|
||||
|
||||
onModalClose();
|
||||
}, [movieId, selectedState, dispatch, onModalClose]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchOrganizePreview({ movieId }));
|
||||
dispatch(fetchNamingSettings());
|
||||
}, [movieId, dispatch]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('OrganizeModalHeader')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>{translate('OrganizeLoadError')}</Alert>
|
||||
) : null}
|
||||
|
||||
{!isFetching && isPopulated && !items.length ? (
|
||||
<div>
|
||||
{renameMovies ? (
|
||||
<div>{translate('OrganizeNothingToRename')}</div>
|
||||
) : (
|
||||
<div>{translate('OrganizeRenamingDisabled')}</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!isFetching && isPopulated && items.length ? (
|
||||
<div>
|
||||
<Alert>
|
||||
<div>
|
||||
<InlineMarkdown
|
||||
data={translate('OrganizeRelativePaths', {
|
||||
path: movie.path,
|
||||
})}
|
||||
blockClassName={styles.path}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InlineMarkdown
|
||||
data={translate('OrganizeNamingPattern', {
|
||||
standardMovieFormat,
|
||||
})}
|
||||
blockClassName={styles.standardMovieFormat}
|
||||
/>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<div className={styles.previews}>
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<OrganizePreviewRow
|
||||
key={item.movieFileId}
|
||||
id={item.movieFileId}
|
||||
existingPath={item.existingPath}
|
||||
newPath={item.newPath}
|
||||
isSelected={selectedState[item.movieFileId]}
|
||||
onSelectedChange={handleSelectedChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{isPopulated && items.length ? (
|
||||
<CheckInput
|
||||
className={styles.selectAllInput}
|
||||
containerClassName={styles.selectAllInputContainer}
|
||||
name="selectAll"
|
||||
value={selectAllValue}
|
||||
onChange={handleSelectAllChange}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<Button kind={kinds.PRIMARY} onPress={handleOrganizePress}>
|
||||
{translate('Organize')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default OrganizePreviewModalContent;
|
||||
@@ -1,88 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchOrganizePreview } from 'Store/Actions/organizePreviewActions';
|
||||
import { fetchNamingSettings } from 'Store/Actions/settingsActions';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import OrganizePreviewModalContent from './OrganizePreviewModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.organizePreview,
|
||||
(state) => state.settings.naming,
|
||||
createMovieSelector(),
|
||||
(organizePreview, naming, movie) => {
|
||||
const props = { ...organizePreview };
|
||||
props.isFetching = organizePreview.isFetching || naming.isFetching;
|
||||
props.isPopulated = organizePreview.isPopulated && naming.isPopulated;
|
||||
props.error = organizePreview.error || naming.error;
|
||||
props.renameMovies = naming.item.renameMovies;
|
||||
props.standardMovieFormat = naming.item.standardMovieFormat;
|
||||
props.path = movie.path;
|
||||
|
||||
return props;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchOrganizePreview,
|
||||
fetchNamingSettings,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class OrganizePreviewModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
movieId
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchOrganizePreview({
|
||||
movieId
|
||||
});
|
||||
|
||||
this.props.fetchNamingSettings();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onOrganizePress = (files) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.RENAME_FILES,
|
||||
movieId: this.props.movieId,
|
||||
files
|
||||
});
|
||||
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<OrganizePreviewModalContent
|
||||
{...this.props}
|
||||
onOrganizePress={this.onOrganizePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OrganizePreviewModalContentConnector.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
fetchOrganizePreview: PropTypes.func.isRequired,
|
||||
fetchNamingSettings: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(OrganizePreviewModalContentConnector);
|
||||
@@ -1,90 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import styles from './OrganizePreviewRow.css';
|
||||
|
||||
class OrganizePreviewRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
id,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value: true });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectedChange = ({ value, shiftKey }) => {
|
||||
const {
|
||||
id,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value, shiftKey });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
existingPath,
|
||||
newPath,
|
||||
isSelected
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<CheckInput
|
||||
containerClassName={styles.selectedContainer}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
onChange={this.onSelectedChange}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.SUBTRACT}
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
|
||||
<span className={styles.path}>
|
||||
{existingPath}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Icon
|
||||
name={icons.ADD}
|
||||
kind={kinds.SUCCESS}
|
||||
/>
|
||||
|
||||
<span className={styles.path}>
|
||||
{newPath}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
OrganizePreviewRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
existingPath: PropTypes.string.isRequired,
|
||||
newPath: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default OrganizePreviewRow;
|
||||
61
frontend/src/Organize/OrganizePreviewRow.tsx
Normal file
61
frontend/src/Organize/OrganizePreviewRow.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React, { useCallback, useEffect } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { CheckInputChanged } from 'typings/inputs';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
import styles from './OrganizePreviewRow.css';
|
||||
|
||||
interface OrganizePreviewRowProps {
|
||||
id: number;
|
||||
existingPath: string;
|
||||
newPath: string;
|
||||
isSelected?: boolean;
|
||||
onSelectedChange: (props: SelectStateInputProps) => void;
|
||||
}
|
||||
|
||||
function OrganizePreviewRow({
|
||||
id,
|
||||
existingPath,
|
||||
newPath,
|
||||
isSelected,
|
||||
onSelectedChange,
|
||||
}: OrganizePreviewRowProps) {
|
||||
const handleSelectedChange = useCallback(
|
||||
({ value, shiftKey }: CheckInputChanged) => {
|
||||
onSelectedChange({ id, value, shiftKey });
|
||||
},
|
||||
[id, onSelectedChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
onSelectedChange({ id, value: true, shiftKey: false });
|
||||
}, [id, onSelectedChange]);
|
||||
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<CheckInput
|
||||
containerClassName={styles.selectedContainer}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
onChange={handleSelectedChange}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div>
|
||||
<Icon name={icons.SUBTRACT} kind={kinds.DANGER} />
|
||||
|
||||
<span className={styles.path}>{existingPath}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Icon name={icons.ADD} kind={kinds.SUCCESS} />
|
||||
|
||||
<span className={styles.path}>{newPath}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default OrganizePreviewRow;
|
||||
@@ -21,8 +21,8 @@
|
||||
display: flex;
|
||||
color: var(--helpTextColor);
|
||||
|
||||
.icon {
|
||||
margin-top: 3px;
|
||||
.identifier {
|
||||
margin-top: 8px;
|
||||
margin-right: 5px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
interface CssExports {
|
||||
'footNote': string;
|
||||
'groups': string;
|
||||
'icon': string;
|
||||
'identifier': string;
|
||||
'namingSelect': string;
|
||||
'namingSelectContainer': string;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useCallback, useState } from 'react';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
@@ -10,7 +9,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { icons, sizes } from 'Helpers/Props';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import NamingConfig from 'typings/Settings/NamingConfig';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import NamingOption from './NamingOption';
|
||||
@@ -88,32 +87,32 @@ const fileNameTokens = [
|
||||
];
|
||||
|
||||
const movieTokens = [
|
||||
{ token: '{Movie Title}', example: "Movie's Title", footNote: true },
|
||||
{ token: '{Movie Title:DE}', example: 'Titel des Films', footNote: true },
|
||||
{ token: '{Movie CleanTitle}', example: 'Movies Title', footNote: true },
|
||||
{ token: '{Movie Title}', example: "Movie's Title", footNotes: '1' },
|
||||
{ token: '{Movie Title:DE}', example: 'Titel des Films', footNotes: '1' },
|
||||
{ token: '{Movie CleanTitle}', example: 'Movies Title', footNotes: '1' },
|
||||
{
|
||||
token: '{Movie CleanTitle:DE}',
|
||||
example: 'Titel des Films',
|
||||
footNote: true,
|
||||
footNotes: '1',
|
||||
},
|
||||
{ token: '{Movie TitleThe}', example: "Movie's Title, The", footNote: true },
|
||||
{ token: '{Movie TitleThe}', example: "Movie's Title, The", footNotes: '1' },
|
||||
{
|
||||
token: '{Movie CleanTitleThe}',
|
||||
example: 'Movies Title, The',
|
||||
footNote: true,
|
||||
footNotes: '1',
|
||||
},
|
||||
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNote: true },
|
||||
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNotes: '1' },
|
||||
{
|
||||
token: '{Movie CleanOriginalTitle}',
|
||||
example: 'Τίτλος ταινίας',
|
||||
footNote: true,
|
||||
footNotes: '1',
|
||||
},
|
||||
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
|
||||
{ token: '{Movie TitleFirstCharacter:DE}', example: 'T' },
|
||||
{
|
||||
token: '{Movie Collection}',
|
||||
example: 'The Movie Collection',
|
||||
footNote: true,
|
||||
footNotes: '1',
|
||||
},
|
||||
{ token: '{Movie Certification}', example: 'R' },
|
||||
{ token: '{Release Year}', example: '2009' },
|
||||
@@ -131,12 +130,21 @@ const qualityTokens = [
|
||||
|
||||
const mediaInfoTokens = [
|
||||
{ token: '{MediaInfo Simple}', example: 'x264 DTS' },
|
||||
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNote: true },
|
||||
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNotes: '1' },
|
||||
|
||||
{ token: '{MediaInfo AudioCodec}', example: 'DTS' },
|
||||
{ token: '{MediaInfo AudioChannels}', example: '5.1' },
|
||||
{ token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: true },
|
||||
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: true },
|
||||
{
|
||||
token: '{MediaInfo AudioLanguages}',
|
||||
example: '[EN+DE]',
|
||||
footNotes: '1,2',
|
||||
},
|
||||
{
|
||||
token: '{MediaInfo AudioLanguagesAll}',
|
||||
example: '[EN]',
|
||||
footNotes: '1',
|
||||
},
|
||||
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNotes: '1' },
|
||||
|
||||
{ token: '{MediaInfo VideoCodec}', example: 'x264' },
|
||||
{ token: '{MediaInfo VideoBitDepth}', example: '10' },
|
||||
@@ -146,11 +154,11 @@ const mediaInfoTokens = [
|
||||
];
|
||||
|
||||
const releaseGroupTokens = [
|
||||
{ token: '{Release Group}', example: 'Rls Grp', footNote: true },
|
||||
{ token: '{Release Group}', example: 'Rls Grp', footNotes: '1' },
|
||||
];
|
||||
|
||||
const editionTokens = [
|
||||
{ token: '{Edition Tags}', example: 'IMAX', footNote: true },
|
||||
{ token: '{Edition Tags}', example: 'IMAX', footNotes: '1' },
|
||||
];
|
||||
|
||||
const customFormatTokens = [
|
||||
@@ -287,13 +295,13 @@ function NamingModal(props: NamingModalProps) {
|
||||
|
||||
<FieldSet legend={translate('Movie')}>
|
||||
<div className={styles.groups}>
|
||||
{movieTokens.map(({ token, example, footNote }) => {
|
||||
{movieTokens.map(({ token, example, footNotes }) => {
|
||||
return (
|
||||
<NamingOption
|
||||
key={token}
|
||||
token={token}
|
||||
example={example}
|
||||
footNote={footNote}
|
||||
footNotes={footNotes}
|
||||
tokenSeparator={tokenSeparator}
|
||||
tokenCase={tokenCase}
|
||||
onPress={handleOptionPress}
|
||||
@@ -303,7 +311,7 @@ function NamingModal(props: NamingModalProps) {
|
||||
</div>
|
||||
|
||||
<div className={styles.footNote}>
|
||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||
<sup className={styles.identifier}>1</sup>
|
||||
<InlineMarkdown data={translate('MovieFootNote')} />
|
||||
</div>
|
||||
</FieldSet>
|
||||
@@ -346,13 +354,13 @@ function NamingModal(props: NamingModalProps) {
|
||||
|
||||
<FieldSet legend={translate('MediaInfo')}>
|
||||
<div className={styles.groups}>
|
||||
{mediaInfoTokens.map(({ token, example, footNote }) => {
|
||||
{mediaInfoTokens.map(({ token, example, footNotes }) => {
|
||||
return (
|
||||
<NamingOption
|
||||
key={token}
|
||||
token={token}
|
||||
example={example}
|
||||
footNote={footNote}
|
||||
footNotes={footNotes}
|
||||
tokenSeparator={tokenSeparator}
|
||||
tokenCase={tokenCase}
|
||||
onPress={handleOptionPress}
|
||||
@@ -362,20 +370,25 @@ function NamingModal(props: NamingModalProps) {
|
||||
</div>
|
||||
|
||||
<div className={styles.footNote}>
|
||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||
<sup className={styles.identifier}>1</sup>
|
||||
<InlineMarkdown data={translate('MediaInfoFootNote')} />
|
||||
</div>
|
||||
|
||||
<div className={styles.footNote}>
|
||||
<sup className={styles.identifier}>2</sup>
|
||||
<InlineMarkdown data={translate('MediaInfoFootNote2')} />
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('ReleaseGroup')}>
|
||||
<div className={styles.groups}>
|
||||
{releaseGroupTokens.map(({ token, example, footNote }) => {
|
||||
{releaseGroupTokens.map(({ token, example, footNotes }) => {
|
||||
return (
|
||||
<NamingOption
|
||||
key={token}
|
||||
token={token}
|
||||
example={example}
|
||||
footNote={footNote}
|
||||
footNotes={footNotes}
|
||||
tokenSeparator={tokenSeparator}
|
||||
tokenCase={tokenCase}
|
||||
onPress={handleOptionPress}
|
||||
@@ -385,20 +398,20 @@ function NamingModal(props: NamingModalProps) {
|
||||
</div>
|
||||
|
||||
<div className={styles.footNote}>
|
||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||
<sup className={styles.identifier}>1</sup>
|
||||
<InlineMarkdown data={translate('ReleaseGroupFootNote')} />
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend={translate('Edition')}>
|
||||
<div className={styles.groups}>
|
||||
{editionTokens.map(({ token, example, footNote }) => {
|
||||
{editionTokens.map(({ token, example, footNotes }) => {
|
||||
return (
|
||||
<NamingOption
|
||||
key={token}
|
||||
token={token}
|
||||
example={example}
|
||||
footNote={footNote}
|
||||
footNotes={footNotes}
|
||||
tokenSeparator={tokenSeparator}
|
||||
tokenCase={tokenCase}
|
||||
onPress={handleOptionPress}
|
||||
@@ -408,7 +421,7 @@ function NamingModal(props: NamingModalProps) {
|
||||
</div>
|
||||
|
||||
<div className={styles.footNote}>
|
||||
<Icon className={styles.icon} name={icons.FOOTNOTE} />
|
||||
<sup className={styles.identifier}>1</sup>
|
||||
<InlineMarkdown data={translate('EditionFootNote')} />
|
||||
</div>
|
||||
</FieldSet>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
padding: 6px;
|
||||
background-color: var(--popoverBodyBackgroundColor);
|
||||
|
||||
.footNote {
|
||||
.footNotes {
|
||||
padding: 2px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'example': string;
|
||||
'footNote': string;
|
||||
'footNotes': string;
|
||||
'isFullFilename': string;
|
||||
'large': string;
|
||||
'lower': string;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { Size } from 'Helpers/Props/sizes';
|
||||
import TokenCase from './TokenCase';
|
||||
import TokenSeparator from './TokenSeparator';
|
||||
@@ -14,7 +12,7 @@ interface NamingOptionProps {
|
||||
example: string;
|
||||
tokenCase: TokenCase;
|
||||
isFullFilename?: boolean;
|
||||
footNote?: boolean;
|
||||
footNotes?: string;
|
||||
size?: Extract<Size, keyof typeof styles>;
|
||||
onPress: ({
|
||||
isFullFilename,
|
||||
@@ -32,7 +30,7 @@ function NamingOption(props: NamingOptionProps) {
|
||||
example,
|
||||
tokenCase,
|
||||
isFullFilename = false,
|
||||
footNote = false,
|
||||
footNotes,
|
||||
size = 'small',
|
||||
onPress,
|
||||
} = props;
|
||||
@@ -66,8 +64,10 @@ function NamingOption(props: NamingOptionProps) {
|
||||
<div className={styles.example}>
|
||||
{example.replace(/ /g, tokenSeparator)}
|
||||
|
||||
{footNote ? (
|
||||
<Icon className={styles.footNote} name={icons.FOOTNOTE} />
|
||||
{footNotes ? (
|
||||
<div className={styles.footNotes}>
|
||||
<sup>{footNotes}</sup>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
import Alert from 'Components/Alert';
|
||||
@@ -13,6 +13,7 @@ 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 usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import {
|
||||
saveMetadata,
|
||||
@@ -41,6 +42,8 @@ function EditMetadataModalContent({
|
||||
(state: AppState) => state.settings.metadata
|
||||
);
|
||||
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
const { settings, ...otherSettings } = useMemo(() => {
|
||||
const item = items.find((item) => item.id === id)!;
|
||||
|
||||
@@ -69,6 +72,12 @@ function EditMetadataModalContent({
|
||||
dispatch(saveMetadata({ id }));
|
||||
}, [id, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (wasSaving && !isSaving && !saveError) {
|
||||
onModalClose();
|
||||
}
|
||||
}, [isSaving, wasSaving, saveError, onModalClose]);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
|
||||
@@ -86,7 +86,7 @@ export const actionHandlers = handleThunks({
|
||||
section,
|
||||
...item,
|
||||
term,
|
||||
queued: true,
|
||||
isQueued: true,
|
||||
items: []
|
||||
}));
|
||||
|
||||
@@ -151,6 +151,8 @@ export const actionHandlers = handleThunks({
|
||||
abortCurrentLookup = abortRequest;
|
||||
|
||||
request.done((data) => {
|
||||
const selectedMovie = queued.selectedMovie || data[0];
|
||||
|
||||
dispatch(updateItem({
|
||||
section,
|
||||
id: queued.id,
|
||||
@@ -158,8 +160,8 @@ export const actionHandlers = handleThunks({
|
||||
isPopulated: true,
|
||||
error: null,
|
||||
items: data,
|
||||
queued: false,
|
||||
selectedMovie: queued.selectedMovie || data[0],
|
||||
isQueued: false,
|
||||
selectedMovie,
|
||||
updateOnly: true
|
||||
}));
|
||||
});
|
||||
@@ -171,7 +173,7 @@ export const actionHandlers = handleThunks({
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: xhr,
|
||||
queued: false,
|
||||
isQueued: false,
|
||||
updateOnly: true
|
||||
}));
|
||||
});
|
||||
@@ -278,7 +280,23 @@ export const actionHandlers = handleThunks({
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[CANCEL_LOOKUP_MOVIE]: function(state) {
|
||||
return Object.assign({}, state, { isLookingUpMovie: false });
|
||||
queue.splice(0, queue.length);
|
||||
|
||||
const items = state.items.map((item) => {
|
||||
if (item.isQueued) {
|
||||
return {
|
||||
...item,
|
||||
isQueued: false
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
return Object.assign({}, state, {
|
||||
isLookingUpMovie: false,
|
||||
items
|
||||
});
|
||||
},
|
||||
|
||||
[CLEAR_IMPORT_MOVIE]: function(state) {
|
||||
|
||||
@@ -210,6 +210,12 @@ export const defaultState = {
|
||||
name: 'rejectionCount',
|
||||
label: () => translate('RejectionCount'),
|
||||
type: filterBuilderTypes.NUMBER
|
||||
},
|
||||
{
|
||||
name: 'movieRequested',
|
||||
label: () => translate('MovieRequested'),
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.BOOL
|
||||
}
|
||||
],
|
||||
selectedFilterKey: 'all'
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
export function createCollectionSelectorForHook(tmdbId: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.movieCollections.items,
|
||||
(collections) => {
|
||||
return collections.find((item) => item.tmdbId === tmdbId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createCollectionSelector() {
|
||||
return createSelector(
|
||||
(_: AppState, { collectionId }: { collectionId: number }) => collectionId,
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
export function createQueueItemSelectorForHook(movieId: number) {
|
||||
return createSelector(
|
||||
(state: AppState) => state.queue.details.items,
|
||||
(details) => {
|
||||
if (!movieId || !details) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return details.find((item) => item.movieId === movieId);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createQueueItemSelector() {
|
||||
return createSelector(
|
||||
(_: AppState, { movieId }: { movieId: number }) => movieId,
|
||||
|
||||
19
frontend/src/Utilities/Number/formatBitrate.ts
Normal file
19
frontend/src/Utilities/Number/formatBitrate.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { filesize } from 'filesize';
|
||||
|
||||
function formatBitrate(input: string | number) {
|
||||
const size = Number(input);
|
||||
|
||||
if (isNaN(size)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const { value, symbol } = filesize(size, {
|
||||
base: 10,
|
||||
round: 1,
|
||||
output: 'object',
|
||||
});
|
||||
|
||||
return `${value} ${symbol}/s`;
|
||||
}
|
||||
|
||||
export default formatBitrate;
|
||||
@@ -6,7 +6,7 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import movieEntities from 'Movie/movieEntities';
|
||||
import MovieSearchCell from 'Movie/MovieSearchCell';
|
||||
import MovieStatusConnector from 'Movie/MovieStatusConnector';
|
||||
import MovieStatus from 'Movie/MovieStatus';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import MovieFileLanguages from 'MovieFile/MovieFileLanguages';
|
||||
import styles from './CutoffUnmetRow.css';
|
||||
@@ -127,7 +127,7 @@ function CutoffUnmetRow(props) {
|
||||
key={name}
|
||||
className={styles.status}
|
||||
>
|
||||
<MovieStatusConnector
|
||||
<MovieStatus
|
||||
movieId={id}
|
||||
movieFileId={movieFileId}
|
||||
movieEntity={movieEntities.WANTED_CUTOFF_UNMET}
|
||||
|
||||
@@ -6,7 +6,7 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import movieEntities from 'Movie/movieEntities';
|
||||
import MovieSearchCell from 'Movie/MovieSearchCell';
|
||||
import MovieStatusConnector from 'Movie/MovieStatusConnector';
|
||||
import MovieStatus from 'Movie/MovieStatus';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import styles from './MissingRow.css';
|
||||
|
||||
@@ -117,7 +117,7 @@ function MissingRow(props) {
|
||||
key={name}
|
||||
className={styles.status}
|
||||
>
|
||||
<MovieStatusConnector
|
||||
<MovieStatus
|
||||
movieId={id}
|
||||
movieFileId={movieFileId}
|
||||
movieEntity={movieEntities.WANTED_MISSING}
|
||||
|
||||
@@ -23,12 +23,13 @@ const error = console.error;
|
||||
function logError(...parameters: any[]) {
|
||||
const filter = parameters.find((parameter) => {
|
||||
return (
|
||||
parameter.includes(
|
||||
typeof parameter === 'string' &&
|
||||
(parameter.includes(
|
||||
'Support for defaultProps will be removed from function components in a future major release'
|
||||
) ||
|
||||
parameter.includes(
|
||||
'findDOMNode is deprecated and will be removed in the next major release'
|
||||
)
|
||||
parameter.includes(
|
||||
'findDOMNode is deprecated and will be removed in the next major release'
|
||||
))
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ interface MovieCollection extends ModelBase {
|
||||
movies: Movie[];
|
||||
missingMovies: number;
|
||||
tags: number[];
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
export default MovieCollection;
|
||||
|
||||
35
frontend/src/typings/Release.ts
Normal file
35
frontend/src/typings/Release.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
|
||||
interface Release {
|
||||
guid: string;
|
||||
protocol: DownloadProtocol;
|
||||
age: number;
|
||||
ageHours: number;
|
||||
ageMinutes: number;
|
||||
publishDate: string;
|
||||
title: string;
|
||||
infoUrl: string;
|
||||
indexerId: number;
|
||||
indexer: string;
|
||||
size: number;
|
||||
seeders?: number;
|
||||
leechers?: number;
|
||||
quality: QualityModel;
|
||||
languages: Language[];
|
||||
customFormats: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
mappedMovieId?: number;
|
||||
indexerFlags: string[];
|
||||
rejections: string[];
|
||||
movieRequested: boolean;
|
||||
downloadAllowed: boolean;
|
||||
|
||||
isGrabbing?: boolean;
|
||||
isGrabbed?: boolean;
|
||||
grabError?: string;
|
||||
}
|
||||
|
||||
export default Release;
|
||||
@@ -5,4 +5,6 @@ export type InputChanged<T = unknown> = {
|
||||
|
||||
export type InputOnChange<T> = (change: InputChanged<T>) => void;
|
||||
|
||||
export type CheckInputChanged = InputChanged<boolean>;
|
||||
export interface CheckInputChanged extends InputChanged<boolean> {
|
||||
shiftKey: boolean;
|
||||
}
|
||||
|
||||
@@ -341,10 +341,11 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
var isCifs = targetDriveFormat == "cifs";
|
||||
var isBtrfs = sourceDriveFormat == "btrfs" && targetDriveFormat == "btrfs";
|
||||
var isZfs = sourceDriveFormat == "zfs" && targetDriveFormat == "zfs";
|
||||
|
||||
if (mode.HasFlag(TransferMode.Copy))
|
||||
{
|
||||
if (isBtrfs)
|
||||
if (isBtrfs || isZfs)
|
||||
{
|
||||
if (_diskProvider.TryCreateRefLink(sourcePath, targetPath))
|
||||
{
|
||||
@@ -358,7 +359,7 @@ namespace NzbDrone.Common.Disk
|
||||
|
||||
if (mode.HasFlag(TransferMode.Move))
|
||||
{
|
||||
if (isBtrfs)
|
||||
if (isBtrfs || isZfs)
|
||||
{
|
||||
if (isSameMount && _diskProvider.TryRenameFile(sourcePath, targetPath))
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user