mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-16 21:15:33 -04:00
Compare commits
205 Commits
v5.15.0.94
...
v5.20.2.97
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
954a040d6e | ||
|
|
905b23618a | ||
|
|
8decd5d8e1 | ||
|
|
8b5b177d16 | ||
|
|
e6c6fceff8 | ||
|
|
dcc8b28a07 | ||
|
|
02baf4d7a4 | ||
|
|
22ec1fe492 | ||
|
|
a7dbdadd21 | ||
|
|
93581e4a2f | ||
|
|
4c8da09df6 | ||
|
|
89666175a6 | ||
|
|
7a33e156a3 | ||
|
|
c7c07404b0 | ||
|
|
abeeee9363 | ||
|
|
23c30734d2 | ||
|
|
939e45e646 | ||
|
|
16ceba2392 | ||
|
|
94d620d878 | ||
|
|
ee0db93a0a | ||
|
|
f815b31c33 | ||
|
|
c078191b3d | ||
|
|
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 | ||
|
|
4051cf3d80 | ||
|
|
9876ed64e2 | ||
|
|
2f26974ecc | ||
|
|
25f66a3029 | ||
|
|
0e25b2708c | ||
|
|
410870d21e | ||
|
|
a64d931904 | ||
|
|
f0a9e76cfc | ||
|
|
6f23c465ee | ||
|
|
af60cca9ae | ||
|
|
d34d23a052 | ||
|
|
0a0da42543 | ||
|
|
e5419f6f06 | ||
|
|
88d9c08f1a | ||
|
|
6b4259757c | ||
|
|
f1d7c56d94 | ||
|
|
c81b2e80ee | ||
|
|
5efefd804b | ||
|
|
38f9543526 | ||
|
|
aae68e681e | ||
|
|
1d21bbf78f | ||
|
|
99c3c8ce5b | ||
|
|
85171e40a5 | ||
|
|
86b656d323 | ||
|
|
5ae5d1043a | ||
|
|
b801aa0935 | ||
|
|
b2b5aa1f79 | ||
|
|
8c6ba9a543 | ||
|
|
4e024c51d3 | ||
|
|
e4106f0ede | ||
|
|
9032ac20ff | ||
|
|
23fce4bf2e | ||
|
|
64fd8552f8 | ||
|
|
e016410c10 | ||
|
|
bea943adf8 | ||
|
|
9780d20f8a | ||
|
|
62722d45b0 | ||
|
|
27dd8e8cd5 | ||
|
|
6c47ede76b | ||
|
|
7b9562bb38 | ||
|
|
8b0b7c1cb0 | ||
|
|
7ebd341cd6 | ||
|
|
6c85f166ff | ||
|
|
45aabce107 | ||
|
|
0caa793df4 | ||
|
|
9a107cc8d7 | ||
|
|
a6d727fe2a | ||
|
|
01a53d3624 | ||
|
|
348c29c9d7 | ||
|
|
64739712c6 | ||
|
|
6ac9cca953 | ||
|
|
a2b38c5b7d | ||
|
|
3cc4105d71 | ||
|
|
3449a5d3fe | ||
|
|
5bac157d36 | ||
|
|
114d260f42 | ||
|
|
617b9c5d35 | ||
|
|
ba4ccbb0bd | ||
|
|
b845268b3d | ||
|
|
0fee552074 | ||
|
|
828b994ef4 | ||
|
|
7952fd325b | ||
|
|
4b4e598b67 | ||
|
|
71ccebd0f5 | ||
|
|
2607c67912 | ||
|
|
a626b4f3c4 | ||
|
|
1526bf29f4 | ||
|
|
2194772736 | ||
|
|
cd490d6334 | ||
|
|
ff609848d8 | ||
|
|
15b6f7212d | ||
|
|
af06a9f70d | ||
|
|
c3fa440cf8 | ||
|
|
0411d66520 | ||
|
|
179637fe8b | ||
|
|
09b4bf15cf | ||
|
|
ea86d14ca7 | ||
|
|
2429dd91c6 | ||
|
|
a752476cdb | ||
|
|
50ce480abf | ||
|
|
0ef6e56e5d | ||
|
|
12d5014125 | ||
|
|
c8301d425c | ||
|
|
b1df9b2401 | ||
|
|
ff09da3a69 | ||
|
|
3b9bd696fb | ||
|
|
9ab3e6bab7 | ||
|
|
86f4f86a0a | ||
|
|
40d95a04e3 | ||
|
|
ca724836ce | ||
|
|
10e3964111 | ||
|
|
b22a86e1d7 | ||
|
|
5976d66511 | ||
|
|
b4eff4d4f9 | ||
|
|
1414a09111 | ||
|
|
b30efd0c62 | ||
|
|
def6950db4 | ||
|
|
f23c2dbaba | ||
|
|
186e9cdd23 |
@@ -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.15.0'
|
||||
majorVersion: '5.20.2'
|
||||
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,10 +1223,10 @@ 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
|
||||
- task: reportgenerator@5.3.11
|
||||
displayName: Generate Coverage Report
|
||||
inputs:
|
||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -210,7 +210,6 @@ module.exports = {
|
||||
'no-undef-init': 'off',
|
||||
'no-undefined': 'off',
|
||||
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
|
||||
'no-use-before-define': 'error',
|
||||
|
||||
// Node.js and CommonJS
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ module.exports = (env) => {
|
||||
const config = {
|
||||
mode: isProduction ? 'production' : 'development',
|
||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||
target: 'web',
|
||||
|
||||
stats: {
|
||||
children: false
|
||||
@@ -186,7 +187,7 @@ module.exports = (env) => {
|
||||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3
|
||||
corejs: '3.39'
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -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>
|
||||
@@ -131,7 +165,7 @@ function HistoryDetails(props: HistoryDetailsProps) {
|
||||
}
|
||||
|
||||
if (eventType === 'downloadFailed') {
|
||||
const { message } = data as DownloadFailedHistory;
|
||||
const { message, indexer } = data as DownloadFailedHistory;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
@@ -145,6 +179,10 @@ function HistoryDetails(props: HistoryDetailsProps) {
|
||||
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
|
||||
) : null}
|
||||
|
||||
{indexer ? (
|
||||
<DescriptionListItem title={translate('Indexer')} data={indexer} />
|
||||
) : null}
|
||||
|
||||
{message ? (
|
||||
<DescriptionListItem title={translate('Message')} data={message} />
|
||||
) : null}
|
||||
|
||||
@@ -37,7 +37,7 @@ interface HistoryDetailsModalProps {
|
||||
sourceTitle: string;
|
||||
data: HistoryData;
|
||||
downloadId?: string;
|
||||
isMarkingAsFailed: boolean;
|
||||
isMarkingAsFailed?: boolean;
|
||||
onMarkAsFailedPress: () => void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
@@ -154,9 +154,14 @@ function HistoryRow(props: HistoryRowProps) {
|
||||
}
|
||||
|
||||
if (name === 'downloadClient') {
|
||||
const downloadClientName =
|
||||
'downloadClientName' in data ? data.downloadClientName : null;
|
||||
const downloadClient =
|
||||
'downloadClient' in data ? data.downloadClient : null;
|
||||
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.downloadClient}>
|
||||
{'downloadClient' in data ? data.downloadClient : ''}
|
||||
{downloadClientName ?? downloadClient ?? ''}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,8 +82,7 @@ class AddNewMovie extends Component {
|
||||
const {
|
||||
error,
|
||||
items,
|
||||
hasExistingMovies,
|
||||
colorImpairedMode
|
||||
hasExistingMovies
|
||||
} = this.props;
|
||||
|
||||
const term = this.state.term;
|
||||
@@ -150,7 +149,6 @@ class AddNewMovie extends Component {
|
||||
return (
|
||||
<AddNewMovieSearchResultConnector
|
||||
key={item.tmdbId}
|
||||
colorImpairedMode={colorImpairedMode}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
@@ -223,8 +221,7 @@ AddNewMovie.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hasExistingMovies: PropTypes.bool.isRequired,
|
||||
onMovieLookupChange: PropTypes.func.isRequired,
|
||||
onClearMovieLookup: PropTypes.func.isRequired,
|
||||
colorImpairedMode: PropTypes.bool.isRequired
|
||||
onClearMovieLookup: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewMovie;
|
||||
|
||||
@@ -6,7 +6,6 @@ import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
|
||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import parseUrl from 'Utilities/String/parseUrl';
|
||||
@@ -17,15 +16,13 @@ function createMapStateToProps() {
|
||||
(state) => state.addMovie,
|
||||
(state) => state.movies.items.length,
|
||||
(state) => state.router.location,
|
||||
createUISettingsSelector(),
|
||||
(addMovie, existingMoviesCount, location, uiSettings) => {
|
||||
(addMovie, existingMoviesCount, location) => {
|
||||
const { params } = parseUrl(location.search);
|
||||
|
||||
return {
|
||||
...addMovie,
|
||||
term: params.term,
|
||||
hasExistingMovies: existingMoviesCount > 0,
|
||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
||||
hasExistingMovies: existingMoviesCount > 0
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -91,6 +91,10 @@
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.genres {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-left: 5px;
|
||||
pointer-events: all;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
import MovieStatusLabel from 'Movie/Details/MovieStatusLabel';
|
||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||
import MovieGenres from 'Movie/MovieGenres';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -73,12 +74,9 @@ class AddNewMovieSearchResult extends Component {
|
||||
isExistingMovie,
|
||||
isExcluded,
|
||||
isSmallScreen,
|
||||
colorImpairedMode,
|
||||
id,
|
||||
monitored,
|
||||
isAvailable,
|
||||
movieFile,
|
||||
queueItem,
|
||||
runtime,
|
||||
movieRuntimeFormat,
|
||||
certification
|
||||
@@ -249,9 +247,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
name={icons.GENRE}
|
||||
size={13}
|
||||
/>
|
||||
<span className={styles.genres}>
|
||||
{genres.slice(0, 3).join(', ')}
|
||||
</span>
|
||||
<MovieGenres className={styles.genres} genres={genres} />
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
@@ -280,20 +276,18 @@ class AddNewMovieSearchResult extends Component {
|
||||
}
|
||||
canFlip={true}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
|
||||
{
|
||||
isExistingMovie && isSmallScreen &&
|
||||
<MovieStatusLabel
|
||||
status={status}
|
||||
hasMovieFiles={hasMovieFile}
|
||||
movieId={existingMovieId}
|
||||
monitored={monitored}
|
||||
isAvailable={isAvailable}
|
||||
queueItem={queueItem}
|
||||
id={id}
|
||||
hasMovieFiles={hasMovieFile}
|
||||
status={status}
|
||||
useLabel={true}
|
||||
colorImpairedMode={colorImpairedMode}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@@ -338,12 +332,9 @@ AddNewMovieSearchResult.propTypes = {
|
||||
isExistingMovie: PropTypes.bool.isRequired,
|
||||
isExcluded: PropTypes.bool,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
id: PropTypes.number,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
isAvailable: PropTypes.bool.isRequired,
|
||||
movieFile: PropTypes.object,
|
||||
queueItem: PropTypes.object,
|
||||
colorImpairedMode: PropTypes.bool,
|
||||
runtime: PropTypes.number.isRequired,
|
||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||
certification: PropTypes.string
|
||||
|
||||
@@ -8,19 +8,16 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createExistingMovieSelector(),
|
||||
createDimensionsSelector(),
|
||||
(state) => state.queue.details.items,
|
||||
(state) => state.movieFiles.items,
|
||||
(state, { internalId }) => internalId,
|
||||
(state) => state.settings.ui.item.movieRuntimeFormat,
|
||||
(isExistingMovie, dimensions, queueItems, movieFiles, internalId, movieRuntimeFormat) => {
|
||||
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
|
||||
(isExistingMovie, dimensions, movieFiles, internalId, movieRuntimeFormat) => {
|
||||
const movieFile = movieFiles.find((item) => internalId > 0 && item.movieId === internalId);
|
||||
|
||||
return {
|
||||
existingMovieId: internalId,
|
||||
isExistingMovie,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
queueItem,
|
||||
movieFile,
|
||||
movieRuntimeFormat
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -10,7 +10,7 @@ import CollectionConnector from 'Collection/CollectionConnector';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
|
||||
import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector';
|
||||
import MovieDetailsPage from 'Movie/Details/MovieDetailsPage';
|
||||
import MovieIndex from 'Movie/Index/MovieIndex';
|
||||
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||
@@ -67,7 +67,7 @@ function AppRoutes() {
|
||||
|
||||
<Route path="/add/discover" component={DiscoverMovieConnector} />
|
||||
|
||||
<Route path="/movie/:titleSlug" component={MovieDetailsPageConnector} />
|
||||
<Route path="/movie/:titleSlug" component={MovieDetailsPage} />
|
||||
|
||||
{/*
|
||||
Calendar
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import Column from 'Components/Table/Column';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
import { ValidationFailure } from 'typings/pending';
|
||||
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
||||
|
||||
export interface Error {
|
||||
responseJSON: {
|
||||
message: string;
|
||||
};
|
||||
status?: number;
|
||||
responseJSON:
|
||||
| {
|
||||
message: string | undefined;
|
||||
}
|
||||
| ValidationFailure[]
|
||||
| undefined;
|
||||
}
|
||||
|
||||
export interface AppSectionDeleteState {
|
||||
@@ -51,6 +56,16 @@ export interface AppSectionItemState<T> {
|
||||
item: T;
|
||||
}
|
||||
|
||||
export interface AppSectionProviderState<T>
|
||||
extends AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
error: Error;
|
||||
items: T[];
|
||||
pendingChanges: Partial<T>;
|
||||
}
|
||||
|
||||
interface AppSectionState<T> {
|
||||
isFetching: boolean;
|
||||
isPopulated: boolean;
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import BlocklistAppState from './BlocklistAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import CommandAppState from './CommandAppState';
|
||||
import HistoryAppState from './HistoryAppState';
|
||||
import ExtraFilesAppState from './ExtraFilesAppState';
|
||||
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';
|
||||
@@ -50,6 +54,7 @@ export interface CustomFilter {
|
||||
export interface AppSectionState {
|
||||
isConnected: boolean;
|
||||
isReconnecting: boolean;
|
||||
isSidebarVisible: boolean;
|
||||
version: string;
|
||||
prevVersion?: string;
|
||||
dimensions: {
|
||||
@@ -64,16 +69,21 @@ interface AppState {
|
||||
blocklist: BlocklistAppState;
|
||||
calendar: CalendarAppState;
|
||||
commands: CommandAppState;
|
||||
extraFiles: ExtraFilesAppState;
|
||||
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;
|
||||
|
||||
6
frontend/src/App/State/ExtraFilesAppState.ts
Normal file
6
frontend/src/App/State/ExtraFilesAppState.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import { ExtraFile } from 'MovieFile/ExtraFile';
|
||||
|
||||
type ExtraFilesAppState = AppSectionState<ExtraFile>;
|
||||
|
||||
export default ExtraFilesAppState;
|
||||
@@ -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>,
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import RecentFolder from 'InteractiveImport/Folder/RecentFolder';
|
||||
import ImportMode from 'InteractiveImport/ImportMode';
|
||||
import InteractiveImport from 'InteractiveImport/InteractiveImport';
|
||||
|
||||
interface FavoriteFolder {
|
||||
folder: string;
|
||||
}
|
||||
|
||||
interface RecentFolder {
|
||||
folder: string;
|
||||
lastUsed: string;
|
||||
}
|
||||
|
||||
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
|
||||
originalItems: InteractiveImport[];
|
||||
importMode: ImportMode;
|
||||
favoriteFolders: FavoriteFolder[];
|
||||
recentFolders: RecentFolder[];
|
||||
}
|
||||
|
||||
|
||||
6
frontend/src/App/State/MetadataAppState.ts
Normal file
6
frontend/src/App/State/MetadataAppState.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { AppSectionProviderState } from 'App/State/AppSectionState';
|
||||
import Metadata from 'typings/Metadata';
|
||||
|
||||
type MetadataAppState = AppSectionProviderState<Metadata>;
|
||||
|
||||
export default MetadataAppState;
|
||||
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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import MovieCredit from 'typings/MovieCredit';
|
||||
|
||||
interface MovieCreditAppState extends AppSectionState<MovieCredit> {}
|
||||
type MovieCreditAppState = AppSectionState<MovieCredit>;
|
||||
|
||||
export default MovieCreditAppState;
|
||||
|
||||
@@ -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;
|
||||
@@ -20,6 +20,7 @@ import NamingConfig from 'typings/Settings/NamingConfig';
|
||||
import NamingExample from 'typings/Settings/NamingExample';
|
||||
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||
import UiSettings from 'typings/Settings/UiSettings';
|
||||
import MetadataAppState from './MetadataAppState';
|
||||
|
||||
export interface DownloadClientAppState
|
||||
extends AppSectionState<DownloadClient>,
|
||||
@@ -36,8 +37,7 @@ export interface NamingAppState
|
||||
extends AppSectionItemState<NamingConfig>,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface NamingExamplesAppState
|
||||
extends AppSectionItemState<NamingExample> {}
|
||||
export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
|
||||
|
||||
export interface ImportListAppState
|
||||
extends AppSectionState<ImportList>,
|
||||
@@ -97,6 +97,7 @@ interface SettingsAppState {
|
||||
indexerFlags: IndexerFlagSettingsAppState;
|
||||
indexers: IndexerAppState;
|
||||
languages: LanguageSettingsAppState;
|
||||
metadata: MetadataAppState;
|
||||
naming: NamingAppState;
|
||||
namingExamples: NamingExamplesAppState;
|
||||
notifications: NotificationAppState;
|
||||
|
||||
@@ -22,7 +22,7 @@ function createMapStateToProps() {
|
||||
return {
|
||||
...collection,
|
||||
movies: [...collection.movies].sort((a, b) => b.year - a.year),
|
||||
genres: Array.from(new Set(allGenres)).slice(0, 3)
|
||||
genres: Array.from(new Set(allGenres))
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -10,7 +10,8 @@ import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import { icons, sizes } from 'Helpers/Props';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import MovieGenres from 'Movie/MovieGenres';
|
||||
import QualityProfileName from 'Settings/Profiles/Quality/QualityProfileName';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import translate from 'Utilities/String/translate';
|
||||
@@ -211,7 +212,7 @@ class CollectionOverview extends Component {
|
||||
/>
|
||||
<span className={styles.qualityProfileName}>
|
||||
{
|
||||
<QualityProfileNameConnector
|
||||
<QualityProfileName
|
||||
qualityProfileId={qualityProfileId}
|
||||
/>
|
||||
}
|
||||
@@ -242,12 +243,10 @@ class CollectionOverview extends Component {
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<Icon
|
||||
name={icons.PROFILE}
|
||||
name={icons.GENRE}
|
||||
size={13}
|
||||
/>
|
||||
<span className={styles.genres}>
|
||||
{genres.join(', ')}
|
||||
</span>
|
||||
<MovieGenres className={styles.genres} genres={genres} />
|
||||
</Label>
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import StackTrace from 'stacktrace-js';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ErrorBoundaryError.css';
|
||||
|
||||
interface ErrorBoundaryErrorProps {
|
||||
@@ -18,7 +19,7 @@ function ErrorBoundaryError(props: ErrorBoundaryErrorProps) {
|
||||
className = styles.container,
|
||||
messageClassName = styles.message,
|
||||
detailsClassName = styles.details,
|
||||
message = 'There was an error loading this content',
|
||||
message = translate('ErrorLoadingContent'),
|
||||
error,
|
||||
info,
|
||||
} = props;
|
||||
|
||||
@@ -16,6 +16,7 @@ import MovieFilterBuilderRowValue from './MovieFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
||||
import QueueStatusFilterBuilderRowValue from './QueueStatusFilterBuilderRowValue';
|
||||
import ReleaseStatusFilterBuilderRowValue from './ReleaseStatusFilterBuilderRowValue';
|
||||
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
||||
import styles from './FilterBuilderRow.css';
|
||||
@@ -80,6 +81,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||
return QualityProfileFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.QUEUE_STATUS:
|
||||
return QueueStatusFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.MOVIE:
|
||||
return MovieFilterBuilderRowValue;
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
const statusTagList = [
|
||||
{
|
||||
id: 'queued',
|
||||
get name() {
|
||||
return translate('Queued');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'paused',
|
||||
get name() {
|
||||
return translate('Paused');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'downloading',
|
||||
get name() {
|
||||
return translate('Downloading');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'completed',
|
||||
get name() {
|
||||
return translate('Completed');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'failed',
|
||||
get name() {
|
||||
return translate('Failed');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'warning',
|
||||
get name() {
|
||||
return translate('Warning');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'delay',
|
||||
get name() {
|
||||
return translate('Delay');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'downloadClientUnavailable',
|
||||
get name() {
|
||||
return translate('DownloadClientUnavailable');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'fallback',
|
||||
get name() {
|
||||
return translate('Fallback');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function QueueStatusFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
return <FilterBuilderRowValue {...props} tagList={statusTagList} />;
|
||||
}
|
||||
|
||||
export default QueueStatusFilterBuilderRowValue;
|
||||
@@ -2,8 +2,11 @@ import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const protocols = [
|
||||
{ id: 'tba', name: 'TBA' },
|
||||
const statusTagList = [
|
||||
{ id: 'tba',
|
||||
get name() {
|
||||
return translate('Tba');
|
||||
} },
|
||||
{
|
||||
id: 'announced',
|
||||
get name() {
|
||||
@@ -33,7 +36,7 @@ const protocols = [
|
||||
function ReleaseStatusFilterBuilderRowValue(props) {
|
||||
return (
|
||||
<FilterBuilderRowValue
|
||||
tagList={protocols}
|
||||
tagList={statusTagList}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -53,7 +53,7 @@ function CustomFiltersModalContent(props) {
|
||||
|
||||
<div className={styles.addButtonContainer}>
|
||||
<Button onPress={onAddCustomFilter}>
|
||||
Add Custom Filter
|
||||
{translate('AddCustomFilter')}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import sortByProp from 'Utilities/Array/sortByProp';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -18,15 +20,17 @@ function createMapStateToProps() {
|
||||
items
|
||||
} = indexers;
|
||||
|
||||
const values = items.sort(sortByProp('name')).map((indexer) => ({
|
||||
key: indexer.id,
|
||||
value: indexer.name
|
||||
}));
|
||||
const values = _.map(items.sort(sortByProp('name')), (indexer) => {
|
||||
return {
|
||||
key: indexer.id,
|
||||
value: indexer.name
|
||||
};
|
||||
});
|
||||
|
||||
if (includeAny) {
|
||||
values.unshift({
|
||||
key: 0,
|
||||
value: '(Any)'
|
||||
value: `(${translate('Any')})`
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import KeyValueListInputItem from './KeyValueListInputItem';
|
||||
import styles from './KeyValueListInput.css';
|
||||
|
||||
class KeyValueListInput extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isFocused: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onItemChange = (index, itemValue) => {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const newValue = [...value];
|
||||
|
||||
if (index == null) {
|
||||
newValue.push(itemValue);
|
||||
} else {
|
||||
newValue.splice(index, 1, itemValue);
|
||||
}
|
||||
|
||||
onChange({
|
||||
name,
|
||||
value: newValue
|
||||
});
|
||||
};
|
||||
|
||||
onRemoveItem = (index) => {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const newValue = [...value];
|
||||
newValue.splice(index, 1);
|
||||
|
||||
onChange({
|
||||
name,
|
||||
value: newValue
|
||||
});
|
||||
};
|
||||
|
||||
onFocus = () => {
|
||||
this.setState({
|
||||
isFocused: true
|
||||
});
|
||||
};
|
||||
|
||||
onBlur = () => {
|
||||
this.setState({
|
||||
isFocused: false
|
||||
});
|
||||
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const newValue = value.reduce((acc, v) => {
|
||||
if (v.key || v.value) {
|
||||
acc.push(v);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (newValue.length !== value.length) {
|
||||
onChange({
|
||||
name,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
value,
|
||||
keyPlaceholder,
|
||||
valuePlaceholder,
|
||||
hasError,
|
||||
hasWarning
|
||||
} = this.props;
|
||||
|
||||
const { isFocused } = this.state;
|
||||
|
||||
return (
|
||||
<div className={classNames(
|
||||
className,
|
||||
isFocused && styles.isFocused,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning
|
||||
)}
|
||||
>
|
||||
{
|
||||
[...value, { key: '', value: '' }].map((v, index) => {
|
||||
return (
|
||||
<KeyValueListInputItem
|
||||
key={index}
|
||||
index={index}
|
||||
keyValue={v.key}
|
||||
value={v.value}
|
||||
keyPlaceholder={keyPlaceholder}
|
||||
valuePlaceholder={valuePlaceholder}
|
||||
isNew={index === value.length}
|
||||
onChange={this.onItemChange}
|
||||
onRemove={this.onRemoveItem}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
KeyValueListInput.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
keyPlaceholder: PropTypes.string,
|
||||
valuePlaceholder: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
KeyValueListInput.defaultProps = {
|
||||
className: styles.inputContainer,
|
||||
value: []
|
||||
};
|
||||
|
||||
export default KeyValueListInput;
|
||||
104
frontend/src/Components/Form/KeyValueListInput.tsx
Normal file
104
frontend/src/Components/Form/KeyValueListInput.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { InputOnChange } from 'typings/inputs';
|
||||
import KeyValueListInputItem from './KeyValueListInputItem';
|
||||
import styles from './KeyValueListInput.css';
|
||||
|
||||
interface KeyValue {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface KeyValueListInputProps {
|
||||
className?: string;
|
||||
name: string;
|
||||
value: KeyValue[];
|
||||
hasError?: boolean;
|
||||
hasWarning?: boolean;
|
||||
keyPlaceholder?: string;
|
||||
valuePlaceholder?: string;
|
||||
onChange: InputOnChange<KeyValue[]>;
|
||||
}
|
||||
|
||||
function KeyValueListInput({
|
||||
className = styles.inputContainer,
|
||||
name,
|
||||
value = [],
|
||||
hasError = false,
|
||||
hasWarning = false,
|
||||
keyPlaceholder,
|
||||
valuePlaceholder,
|
||||
onChange,
|
||||
}: KeyValueListInputProps): JSX.Element {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const handleItemChange = useCallback(
|
||||
(index: number | null, itemValue: KeyValue) => {
|
||||
const newValue = [...value];
|
||||
|
||||
if (index === null) {
|
||||
newValue.push(itemValue);
|
||||
} else {
|
||||
newValue.splice(index, 1, itemValue);
|
||||
}
|
||||
|
||||
onChange({ name, value: newValue });
|
||||
},
|
||||
[value, name, onChange]
|
||||
);
|
||||
|
||||
const handleRemoveItem = useCallback(
|
||||
(index: number) => {
|
||||
const newValue = [...value];
|
||||
newValue.splice(index, 1);
|
||||
onChange({ name, value: newValue });
|
||||
},
|
||||
[value, name, onChange]
|
||||
);
|
||||
|
||||
const onFocus = useCallback(() => setIsFocused(true), []);
|
||||
|
||||
const onBlur = useCallback(() => {
|
||||
setIsFocused(false);
|
||||
|
||||
const newValue = value.reduce((acc: KeyValue[], v) => {
|
||||
if (v.key || v.value) {
|
||||
acc.push(v);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (newValue.length !== value.length) {
|
||||
onChange({ name, value: newValue });
|
||||
}
|
||||
}, [value, name, onChange]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
isFocused && styles.isFocused,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning
|
||||
)}
|
||||
>
|
||||
{[...value, { key: '', value: '' }].map((v, index) => (
|
||||
<KeyValueListInputItem
|
||||
key={index}
|
||||
index={index}
|
||||
keyValue={v.key}
|
||||
value={v.value}
|
||||
keyPlaceholder={keyPlaceholder}
|
||||
valuePlaceholder={valuePlaceholder}
|
||||
isNew={index === value.length}
|
||||
onChange={handleItemChange}
|
||||
onRemove={handleRemoveItem}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default KeyValueListInput;
|
||||
@@ -5,13 +5,19 @@
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
.keyInputWrapper {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.valueInputWrapper {
|
||||
flex: 1 0 0;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
flex: 0 0 22px;
|
||||
}
|
||||
@@ -20,4 +26,10 @@
|
||||
.valueInput {
|
||||
width: 100%;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: var(--textColor);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--helpTextColor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'buttonWrapper': string;
|
||||
'inputWrapper': string;
|
||||
'itemContainer': string;
|
||||
'keyInput': string;
|
||||
'keyInputWrapper': string;
|
||||
'valueInput': string;
|
||||
'valueInputWrapper': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './KeyValueListInputItem.css';
|
||||
|
||||
class KeyValueListInputItem extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onKeyChange = ({ value: keyValue }) => {
|
||||
const {
|
||||
index,
|
||||
value,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
onChange(index, { key: keyValue, value });
|
||||
};
|
||||
|
||||
onValueChange = ({ value }) => {
|
||||
// TODO: Validate here or validate at a lower level component
|
||||
|
||||
const {
|
||||
index,
|
||||
keyValue,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
onChange(index, { key: keyValue, value });
|
||||
};
|
||||
|
||||
onRemovePress = () => {
|
||||
const {
|
||||
index,
|
||||
onRemove
|
||||
} = this.props;
|
||||
|
||||
onRemove(index);
|
||||
};
|
||||
|
||||
onFocus = () => {
|
||||
this.props.onFocus();
|
||||
};
|
||||
|
||||
onBlur = () => {
|
||||
this.props.onBlur();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
keyValue,
|
||||
value,
|
||||
keyPlaceholder,
|
||||
valuePlaceholder,
|
||||
isNew
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.itemContainer}>
|
||||
<div className={styles.inputWrapper}>
|
||||
<TextInput
|
||||
className={styles.keyInput}
|
||||
name="key"
|
||||
value={keyValue}
|
||||
placeholder={keyPlaceholder}
|
||||
onChange={this.onKeyChange}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputWrapper}>
|
||||
<TextInput
|
||||
className={styles.valueInput}
|
||||
name="value"
|
||||
value={value}
|
||||
placeholder={valuePlaceholder}
|
||||
onChange={this.onValueChange}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonWrapper}>
|
||||
{
|
||||
isNew ?
|
||||
null :
|
||||
<IconButton
|
||||
name={icons.REMOVE}
|
||||
tabIndex={-1}
|
||||
onPress={this.onRemovePress}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
KeyValueListInputItem.propTypes = {
|
||||
index: PropTypes.number,
|
||||
keyValue: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
keyPlaceholder: PropTypes.string.isRequired,
|
||||
valuePlaceholder: PropTypes.string.isRequired,
|
||||
isNew: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onRemove: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func.isRequired,
|
||||
onBlur: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
KeyValueListInputItem.defaultProps = {
|
||||
keyPlaceholder: 'Key',
|
||||
valuePlaceholder: 'Value'
|
||||
};
|
||||
|
||||
export default KeyValueListInputItem;
|
||||
89
frontend/src/Components/Form/KeyValueListInputItem.tsx
Normal file
89
frontend/src/Components/Form/KeyValueListInputItem.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './KeyValueListInputItem.css';
|
||||
|
||||
interface KeyValueListInputItemProps {
|
||||
index: number;
|
||||
keyValue: string;
|
||||
value: string;
|
||||
keyPlaceholder?: string;
|
||||
valuePlaceholder?: string;
|
||||
isNew: boolean;
|
||||
onChange: (index: number, itemValue: { key: string; value: string }) => void;
|
||||
onRemove: (index: number) => void;
|
||||
onFocus: () => void;
|
||||
onBlur: () => void;
|
||||
}
|
||||
|
||||
function KeyValueListInputItem({
|
||||
index,
|
||||
keyValue,
|
||||
value,
|
||||
keyPlaceholder = 'Key',
|
||||
valuePlaceholder = 'Value',
|
||||
isNew,
|
||||
onChange,
|
||||
onRemove,
|
||||
onFocus,
|
||||
onBlur,
|
||||
}: KeyValueListInputItemProps): JSX.Element {
|
||||
const handleKeyChange = useCallback(
|
||||
({ value: keyValue }: { value: string }) => {
|
||||
onChange(index, { key: keyValue, value });
|
||||
},
|
||||
[index, value, onChange]
|
||||
);
|
||||
|
||||
const handleValueChange = useCallback(
|
||||
({ value }: { value: string }) => {
|
||||
onChange(index, { key: keyValue, value });
|
||||
},
|
||||
[index, keyValue, onChange]
|
||||
);
|
||||
|
||||
const handleRemovePress = useCallback(() => {
|
||||
onRemove(index);
|
||||
}, [index, onRemove]);
|
||||
|
||||
return (
|
||||
<div className={styles.itemContainer}>
|
||||
<div className={styles.keyInputWrapper}>
|
||||
<TextInput
|
||||
className={styles.keyInput}
|
||||
name="key"
|
||||
value={keyValue}
|
||||
placeholder={keyPlaceholder}
|
||||
onChange={handleKeyChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.valueInputWrapper}>
|
||||
<TextInput
|
||||
className={styles.valueInput}
|
||||
name="value"
|
||||
value={value}
|
||||
placeholder={valuePlaceholder}
|
||||
onChange={handleValueChange}
|
||||
onFocus={onFocus}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonWrapper}>
|
||||
{isNew ? null : (
|
||||
<IconButton
|
||||
name={icons.REMOVE}
|
||||
tabIndex={-1}
|
||||
onPress={handleRemovePress}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default KeyValueListInputItem;
|
||||
@@ -14,6 +14,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
||||
return inputTypes.CHECK;
|
||||
case 'device':
|
||||
return inputTypes.DEVICE;
|
||||
case 'keyValueList':
|
||||
return inputTypes.KEY_VALUE_LIST;
|
||||
case 'password':
|
||||
return inputTypes.PASSWORD;
|
||||
case 'number':
|
||||
@@ -137,6 +139,8 @@ ProviderFieldFormGroup.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
advanced: PropTypes.bool.isRequired,
|
||||
hidden: PropTypes.string,
|
||||
isDisabled: PropTypes.bool,
|
||||
provider: PropTypes.string,
|
||||
pending: PropTypes.bool.isRequired,
|
||||
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
||||
@@ -33,7 +33,7 @@ function createMapStateToProps() {
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
value: `(${translate('Mixed')})`,
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ function createMapStateToProps() {
|
||||
|
||||
values.push({
|
||||
key: ADD_NEW_KEY,
|
||||
value: 'Add a new path'
|
||||
value: translate('AddANewPath')
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInputOption from './EnhancedSelectInputOption';
|
||||
import styles from './RootFolderSelectInputOption.css';
|
||||
|
||||
@@ -47,14 +48,14 @@ function RootFolderSelectInputOption(props) {
|
||||
freeSpace == null ?
|
||||
null :
|
||||
<div className={styles.freeSpace}>
|
||||
{formatBytes(freeSpace)} Free
|
||||
{translate('RootFolderSelectFreeSpace', { freeSpace: formatBytes(freeSpace) })}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isMissing ?
|
||||
<div className={styles.isMissing}>
|
||||
Missing
|
||||
{translate('Missing')}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
@@ -67,8 +68,8 @@ RootFolderSelectInputOption.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
freeSpace: PropTypes.number,
|
||||
movieFolder: PropTypes.string,
|
||||
isMissing: PropTypes.bool,
|
||||
movieFolder: PropTypes.string,
|
||||
isMobile: PropTypes.bool.isRequired,
|
||||
isWindows: PropTypes.bool
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
|
||||
import styles from './RootFolderSelectInputSelectedValue.css';
|
||||
|
||||
@@ -39,7 +40,7 @@ function RootFolderSelectInputSelectedValue(props) {
|
||||
{
|
||||
freeSpace != null && includeFreeSpace &&
|
||||
<div className={styles.freeSpace}>
|
||||
{formatBytes(freeSpace)} Free
|
||||
{translate('RootFolderSelectFreeSpace', { freeSpace: formatBytes(freeSpace) })}
|
||||
</div>
|
||||
}
|
||||
</EnhancedSelectInputSelectedValue>
|
||||
|
||||
@@ -8,27 +8,37 @@ import styles from './UMaskInput.css';
|
||||
const umaskOptions = [
|
||||
{
|
||||
key: '755',
|
||||
value: '755 - Owner write, Everyone else read',
|
||||
get value() {
|
||||
return translate('Umask755Description', { octal: '755' });
|
||||
},
|
||||
hint: 'drwxr-xr-x'
|
||||
},
|
||||
{
|
||||
key: '775',
|
||||
value: '775 - Owner & Group write, Other read',
|
||||
get value() {
|
||||
return translate('Umask775Description', { octal: '775' });
|
||||
},
|
||||
hint: 'drwxrwxr-x'
|
||||
},
|
||||
{
|
||||
key: '770',
|
||||
value: '770 - Owner & Group write',
|
||||
get value() {
|
||||
return translate('Umask770Description', { octal: '770' });
|
||||
},
|
||||
hint: 'drwxrwx---'
|
||||
},
|
||||
{
|
||||
key: '750',
|
||||
value: '750 - Owner write, Group read',
|
||||
get value() {
|
||||
return translate('Umask750Description', { octal: '750' });
|
||||
},
|
||||
hint: 'drwxr-x---'
|
||||
},
|
||||
{
|
||||
key: '777',
|
||||
value: '777 - Everyone write',
|
||||
get value() {
|
||||
return translate('Umask777Description', { octal: '777' });
|
||||
},
|
||||
hint: 'drwxrwxrwx'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -8,17 +8,20 @@ import { kinds } from 'Helpers/Props';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import styles from './Icon.css';
|
||||
|
||||
export type IconName = FontAwesomeIconProps['icon'];
|
||||
export type IconKind = Extract<Kind, keyof typeof styles>;
|
||||
|
||||
export interface IconProps
|
||||
extends Omit<
|
||||
FontAwesomeIconProps,
|
||||
'icon' | 'spin' | 'name' | 'title' | 'size'
|
||||
> {
|
||||
containerClassName?: ComponentProps<'span'>['className'];
|
||||
name: FontAwesomeIconProps['icon'];
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
name: IconName;
|
||||
kind?: IconKind;
|
||||
size?: number;
|
||||
isSpinning?: FontAwesomeIconProps['spin'];
|
||||
title?: string | (() => string);
|
||||
title?: string | (() => string) | null;
|
||||
}
|
||||
|
||||
export default function Icon({
|
||||
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
/** Kinds **/
|
||||
|
||||
.default {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/** Sizes **/
|
||||
|
||||
.small {
|
||||
|
||||
1
frontend/src/Components/InfoLabel.css.d.ts
vendored
1
frontend/src/Components/InfoLabel.css.d.ts
vendored
@@ -1,6 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'default': string;
|
||||
'label': string;
|
||||
'large': string;
|
||||
'medium': string;
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import styles from './InfoLabel.css';
|
||||
|
||||
function InfoLabel(props) {
|
||||
const {
|
||||
className,
|
||||
name,
|
||||
kind,
|
||||
size,
|
||||
outline,
|
||||
children,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
outline && styles.outline
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.name}>
|
||||
{name}
|
||||
</div>
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
InfoLabel.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||
outline: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
InfoLabel.defaultProps = {
|
||||
className: styles.label,
|
||||
kind: kinds.DEFAULT,
|
||||
size: sizes.SMALL,
|
||||
outline: false
|
||||
};
|
||||
|
||||
export default InfoLabel;
|
||||
41
frontend/src/Components/InfoLabel.tsx
Normal file
41
frontend/src/Components/InfoLabel.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { ComponentProps, ReactNode } from 'react';
|
||||
import { Kind } from 'Helpers/Props/kinds';
|
||||
import { Size } from 'Helpers/Props/sizes';
|
||||
import styles from './InfoLabel.css';
|
||||
|
||||
interface InfoLabelProps extends ComponentProps<'span'> {
|
||||
className?: string;
|
||||
name: string;
|
||||
kind?: Extract<Kind, keyof typeof styles>;
|
||||
size?: Extract<Size, keyof typeof styles>;
|
||||
outline?: boolean;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function InfoLabel({
|
||||
className = styles.label,
|
||||
name,
|
||||
kind = 'default',
|
||||
size = 'small',
|
||||
outline = false,
|
||||
children,
|
||||
...otherProps
|
||||
}: InfoLabelProps) {
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
styles[size],
|
||||
outline && styles.outline
|
||||
)}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.name}>{name}</div>
|
||||
<div>{children}</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export default InfoLabel;
|
||||
@@ -1,3 +1,4 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
@@ -7,26 +8,15 @@ const TIMEOUT = 1 / FPS * 1000;
|
||||
|
||||
class Marquee extends Component {
|
||||
|
||||
static propTypes = {
|
||||
text: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
hoverToStop: PropTypes.bool,
|
||||
loop: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
};
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
static defaultProps = {
|
||||
text: '',
|
||||
title: '',
|
||||
hoverToStop: true,
|
||||
loop: false
|
||||
};
|
||||
|
||||
state = {
|
||||
animatedWidth: 0,
|
||||
overflowWidth: 0,
|
||||
direction: 0
|
||||
};
|
||||
this.state = {
|
||||
animatedWidth: 0,
|
||||
overflowWidth: 0,
|
||||
direction: 0
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.measureText();
|
||||
@@ -138,7 +128,7 @@ class Marquee extends Component {
|
||||
ref={(el) => {
|
||||
this.container = el;
|
||||
}}
|
||||
className={`ui-marquee ${this.props.className}`}
|
||||
className={classNames('ui-marquee', this.props.className)}
|
||||
style={{ overflow: 'hidden' }}
|
||||
>
|
||||
<span
|
||||
@@ -159,7 +149,7 @@ class Marquee extends Component {
|
||||
ref={(el) => {
|
||||
this.container = el;
|
||||
}}
|
||||
className={`ui-marquee ${this.props.className}`.trim()}
|
||||
className={classNames('ui-marquee', this.props.className)}
|
||||
style={{ overflow: 'hidden' }}
|
||||
onMouseEnter={this.onHandleMouseEnter}
|
||||
onMouseLeave={this.onHandleMouseLeave}
|
||||
@@ -178,4 +168,20 @@ class Marquee extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
Marquee.propTypes = {
|
||||
text: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
hoverToStop: PropTypes.bool,
|
||||
loop: PropTypes.bool,
|
||||
className: PropTypes.string
|
||||
};
|
||||
|
||||
Marquee.defaultProps = {
|
||||
text: '',
|
||||
title: '',
|
||||
hoverToStop: true,
|
||||
loop: false,
|
||||
className: ''
|
||||
};
|
||||
|
||||
export default Marquee;
|
||||
|
||||
@@ -58,9 +58,9 @@ class FilterMenu extends Component {
|
||||
>
|
||||
<ButtonComponent
|
||||
iconName={icons.FILTER}
|
||||
showIndicator={selectedFilterKey !== 'all'}
|
||||
text={translate('Filter')}
|
||||
isDisabled={isDisabled}
|
||||
showIndicator={selectedFilterKey !== 'all'}
|
||||
/>
|
||||
|
||||
<FilterMenuContent
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import Portal from 'Components/Portal';
|
||||
import { align } from 'Helpers/Props';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import getUniqueElementId from 'Utilities/getUniqueElementId';
|
||||
import styles from './Menu.css';
|
||||
|
||||
const sharedPopperOptions = {
|
||||
@@ -38,8 +38,8 @@ class Menu extends Component {
|
||||
super(props, context);
|
||||
|
||||
this._scheduleUpdate = null;
|
||||
this._menuButtonId = getUniqueElememtId();
|
||||
this._menuContentId = getUniqueElememtId();
|
||||
this._menuButtonId = getUniqueElementId();
|
||||
this._menuContentId = getUniqueElementId();
|
||||
|
||||
this.state = {
|
||||
isMenuOpen: false,
|
||||
|
||||
@@ -18,7 +18,7 @@ function ModalError(props) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Error
|
||||
{translate('Error')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -26,7 +26,7 @@ function ModalError(props) {
|
||||
messageClassName={styles.message}
|
||||
detailsClassName={styles.details}
|
||||
{...otherProps}
|
||||
message={translate('ThereWasAnErrorLoadingThisItem')}
|
||||
message={translate('ErrorLoadingItem')}
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ function KeyboardShortcutsModalContent(props) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Keyboard Shortcuts
|
||||
{translate('KeyboardShortcuts')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
|
||||
@@ -92,7 +92,7 @@ class MovieSearchInput extends Component {
|
||||
if (item.type === ADD_NEW_TYPE) {
|
||||
return (
|
||||
<div className={styles.addNewMovieSuggestion}>
|
||||
Search for {query}
|
||||
{translate('SearchForQuery', { query })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class PageHeader extends Component {
|
||||
<IconButton
|
||||
className={styles.donate}
|
||||
name={icons.HEART}
|
||||
aria-label="Donate"
|
||||
aria-label={translate('Donate')}
|
||||
to="https://radarr.video/donate"
|
||||
size={14}
|
||||
title={translate('Donate')}
|
||||
|
||||
@@ -10,7 +10,7 @@ function PageContentError(props) {
|
||||
<PageContentBody>
|
||||
<ErrorBoundaryError
|
||||
{...props}
|
||||
message={translate('ThereWasAnErrorLoadingThisPage')}
|
||||
message={translate('ErrorLoadingPage')}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
composes: link;
|
||||
|
||||
padding: 10px 24px;
|
||||
padding-left: 35px;
|
||||
}
|
||||
|
||||
.isActiveLink {
|
||||
@@ -41,10 +42,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.noIcon {
|
||||
margin-left: 25px;
|
||||
}
|
||||
|
||||
.status {
|
||||
float: right;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ interface CssExports {
|
||||
'isActiveParentLink': string;
|
||||
'item': string;
|
||||
'link': string;
|
||||
'noIcon': string;
|
||||
'status': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -63,9 +63,7 @@ class PageSidebarItem extends Component {
|
||||
</span>
|
||||
}
|
||||
|
||||
<span className={isChildItem ? styles.noIcon : null}>
|
||||
{typeof title === 'function' ? title() : title}
|
||||
</span>
|
||||
{typeof title === 'function' ? title() : title}
|
||||
|
||||
{
|
||||
!!StatusComponent &&
|
||||
|
||||
@@ -23,11 +23,14 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding: 0 3px;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
color: var(--toolbarLabelColor);
|
||||
font-size: $extraSmallFontSize;
|
||||
line-height: calc($extraSmallFontSize + 1px);
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import styles from './PageToolbarButton.css';
|
||||
|
||||
function PageToolbarButton(props) {
|
||||
const {
|
||||
label,
|
||||
iconName,
|
||||
spinningName,
|
||||
isDisabled,
|
||||
isSpinning,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.toolbarButton,
|
||||
isDisabled && styles.isDisabled
|
||||
)}
|
||||
isDisabled={isDisabled || isSpinning}
|
||||
{...otherProps}
|
||||
>
|
||||
<Icon
|
||||
name={isSpinning ? (spinningName || iconName) : iconName}
|
||||
isSpinning={isSpinning}
|
||||
size={21}
|
||||
/>
|
||||
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
PageToolbarButton.propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
iconName: PropTypes.object.isRequired,
|
||||
spinningName: PropTypes.object,
|
||||
isSpinning: PropTypes.bool,
|
||||
isDisabled: PropTypes.bool,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
|
||||
PageToolbarButton.defaultProps = {
|
||||
spinningName: icons.SPINNER,
|
||||
isDisabled: false,
|
||||
isSpinning: false
|
||||
};
|
||||
|
||||
export default PageToolbarButton;
|
||||
50
frontend/src/Components/Page/Toolbar/PageToolbarButton.tsx
Normal file
50
frontend/src/Components/Page/Toolbar/PageToolbarButton.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import Icon, { IconName } from 'Components/Icon';
|
||||
import Link, { LinkProps } from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import styles from './PageToolbarButton.css';
|
||||
|
||||
export interface PageToolbarButtonProps extends LinkProps {
|
||||
label: string;
|
||||
iconName: IconName;
|
||||
spinningName?: IconName;
|
||||
isSpinning?: boolean;
|
||||
isDisabled?: boolean;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
overflowComponent?: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
function PageToolbarButton({
|
||||
label,
|
||||
iconName,
|
||||
spinningName = icons.SPINNER,
|
||||
isDisabled = false,
|
||||
isSpinning = false,
|
||||
overflowComponent,
|
||||
...otherProps
|
||||
}: PageToolbarButtonProps) {
|
||||
return (
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.toolbarButton,
|
||||
isDisabled && styles.isDisabled
|
||||
)}
|
||||
isDisabled={isDisabled || isSpinning}
|
||||
title={label}
|
||||
{...otherProps}
|
||||
>
|
||||
<Icon
|
||||
name={isSpinning ? spinningName || iconName : iconName}
|
||||
isSpinning={isSpinning}
|
||||
size={21}
|
||||
/>
|
||||
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>{label}</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageToolbarButton;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
import styles from './TableRowCell.css';
|
||||
|
||||
export interface TableRowCellProps extends ComponentPropsWithoutRef<'td'> {}
|
||||
export type TableRowCellProps = ComponentPropsWithoutRef<'td'>;
|
||||
|
||||
export default function TableRowCell({
|
||||
className = styles.cell,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -170,11 +170,11 @@ class TableOptionsModal extends Component {
|
||||
{
|
||||
canModifyColumns ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Columns')}</FormLabel>
|
||||
<FormLabel>{translate('TableColumns')}</FormLabel>
|
||||
|
||||
<div>
|
||||
<FormInputHelpText
|
||||
text={translate('TableOptionsColumnsMessage')}
|
||||
text={translate('TableColumnsHelpText')}
|
||||
/>
|
||||
|
||||
<div className={styles.columns}>
|
||||
|
||||
@@ -6,6 +6,7 @@ import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './TablePager.css';
|
||||
|
||||
class TablePager extends Component {
|
||||
@@ -156,7 +157,7 @@ class TablePager extends Component {
|
||||
|
||||
<div className={styles.recordsContainer}>
|
||||
<div className={styles.records}>
|
||||
Total records: {totalRecords}
|
||||
{translate('TotalRecords', { totalRecords })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -7,63 +7,63 @@ export const shortcuts = {
|
||||
OPEN_KEYBOARD_SHORTCUTS_MODAL: {
|
||||
key: '?',
|
||||
get name() {
|
||||
return translate('OpenThisModal');
|
||||
return translate('KeyboardShortcutsOpenModal');
|
||||
}
|
||||
},
|
||||
|
||||
CLOSE_MODAL: {
|
||||
key: 'Esc',
|
||||
get name() {
|
||||
return translate('CloseCurrentModal');
|
||||
return translate('KeyboardShortcutsCloseModal');
|
||||
}
|
||||
},
|
||||
|
||||
ACCEPT_CONFIRM_MODAL: {
|
||||
key: 'Enter',
|
||||
get name() {
|
||||
return translate('AcceptConfirmationModal');
|
||||
return translate('KeyboardShortcutsConfirmModal');
|
||||
}
|
||||
},
|
||||
|
||||
MOVIE_SEARCH_INPUT: {
|
||||
key: 's',
|
||||
get name() {
|
||||
return translate('FocusSearchBox');
|
||||
return translate('KeyboardShortcutsFocusSearchBox');
|
||||
}
|
||||
},
|
||||
|
||||
SAVE_SETTINGS: {
|
||||
key: 'mod+s',
|
||||
get name() {
|
||||
return translate('SaveSettings');
|
||||
return translate('KeyboardShortcutsSaveSettings');
|
||||
}
|
||||
},
|
||||
|
||||
SCROLL_TOP: {
|
||||
key: 'mod+home',
|
||||
get name() {
|
||||
return translate('MovieIndexScrollTop');
|
||||
return translate('KeyboardShortcutsMovieIndexScrollTop');
|
||||
}
|
||||
},
|
||||
|
||||
SCROLL_BOTTOM: {
|
||||
key: 'mod+end',
|
||||
get name() {
|
||||
return translate('MovieIndexScrollBottom');
|
||||
return translate('KeyboardShortcutsMovieIndexScrollBottom');
|
||||
}
|
||||
},
|
||||
|
||||
DETAILS_NEXT: {
|
||||
key: '→',
|
||||
get name() {
|
||||
return translate('MovieDetailsNextMovie');
|
||||
return translate('KeyboardShortcutsMovieDetailsNextMovie');
|
||||
}
|
||||
},
|
||||
|
||||
DETAILS_PREVIOUS: {
|
||||
key: '←',
|
||||
get name() {
|
||||
return translate('MovieDetailsPreviousMovie');
|
||||
return translate('KeyboardShortcutsMovieDetailsPreviousMovie');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "Radarr",
|
||||
"name": "__INSTANCE_NAME__",
|
||||
"icons": [
|
||||
{
|
||||
"src": "__URL_BASE__/Content/Images/Icons/android-chrome-192x192.png",
|
||||
|
||||
@@ -85,10 +85,16 @@ $hoverScale: 1.05;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.overviewContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 1 1000px;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.overview {
|
||||
composes: link;
|
||||
|
||||
flex: 0 1 1000px;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ interface CssExports {
|
||||
'link': string;
|
||||
'lists': string;
|
||||
'overview': string;
|
||||
'overviewContainer': string;
|
||||
'poster': string;
|
||||
'posterContainer': string;
|
||||
'title': string;
|
||||
|
||||
@@ -133,14 +133,20 @@ class DiscoverMovieOverview extends Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MoviePoster
|
||||
className={styles.poster}
|
||||
<Link
|
||||
className={styles.link}
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
/>
|
||||
{...linkProps}
|
||||
>
|
||||
<MoviePoster
|
||||
className={styles.poster}
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -242,11 +248,13 @@ class DiscoverMovieOverview extends Component {
|
||||
</div>
|
||||
|
||||
<div className={styles.details}>
|
||||
<div className={styles.overview}>
|
||||
<TextTruncate
|
||||
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
||||
text={overview}
|
||||
/>
|
||||
<div className={styles.overviewContainer}>
|
||||
<Link className={styles.overview} {...linkProps}>
|
||||
<TextTruncate
|
||||
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
||||
text={overview}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<DiscoverMovieOverviewInfo
|
||||
@@ -255,7 +263,6 @@ class DiscoverMovieOverview extends Component {
|
||||
{...overviewOptions}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +49,7 @@ function isVisible(row, props) {
|
||||
valueProp
|
||||
} = row;
|
||||
|
||||
return _.has(props, valueProp) && (_.get(props, showProp) || props.sortKey === name);
|
||||
return _.has(props, valueProp) && _.get(props, valueProp) !== null && (props[showProp] || props.sortKey === name);
|
||||
}
|
||||
|
||||
function getInfoRowProps(row, props) {
|
||||
|
||||
@@ -50,15 +50,13 @@ function DiscoverMoviePosterInfo(props) {
|
||||
}
|
||||
|
||||
if (sortKey === 'inCinemas' && inCinemas) {
|
||||
const inCinemasDate = getRelativeDate(
|
||||
inCinemas,
|
||||
const inCinemasDate = getRelativeDate({
|
||||
date: inCinemas,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: false
|
||||
}
|
||||
);
|
||||
timeFormat,
|
||||
timeForToday: false
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.info} title={translate('InCinemas')}>
|
||||
@@ -68,15 +66,13 @@ function DiscoverMoviePosterInfo(props) {
|
||||
}
|
||||
|
||||
if (sortKey === 'digitalRelease' && digitalRelease) {
|
||||
const digitalReleaseDate = getRelativeDate(
|
||||
digitalRelease,
|
||||
const digitalReleaseDate = getRelativeDate({
|
||||
date: digitalRelease,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: false
|
||||
}
|
||||
);
|
||||
timeFormat,
|
||||
timeForToday: false
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.info} title={translate('DigitalRelease')}>
|
||||
@@ -86,15 +82,13 @@ function DiscoverMoviePosterInfo(props) {
|
||||
}
|
||||
|
||||
if (sortKey === 'physicalRelease' && physicalRelease) {
|
||||
const physicalReleaseDate = getRelativeDate(
|
||||
physicalRelease,
|
||||
const physicalReleaseDate = getRelativeDate({
|
||||
date: physicalRelease,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: false
|
||||
}
|
||||
);
|
||||
timeFormat,
|
||||
timeForToday: false
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.info} title={translate('PhysicalRelease')}>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import translate from 'Utilities/String/translate';
|
||||
import * as filterTypes from './filterTypes';
|
||||
|
||||
export const ARRAY = 'array';
|
||||
@@ -20,49 +21,127 @@ export const all = [
|
||||
|
||||
export const possibleFilterTypes = {
|
||||
[ARRAY]: [
|
||||
{ key: filterTypes.CONTAINS, value: 'contains' },
|
||||
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' }
|
||||
{
|
||||
key: filterTypes.CONTAINS,
|
||||
value: () => translate('FilterContains')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_CONTAINS,
|
||||
value: () => translate('FilterDoesNotContain')
|
||||
}
|
||||
],
|
||||
|
||||
[CONTAINS]: [
|
||||
{ key: filterTypes.CONTAINS, value: 'contains' }
|
||||
{
|
||||
key: filterTypes.CONTAINS,
|
||||
value: () => translate('FilterContains')
|
||||
}
|
||||
],
|
||||
|
||||
[DATE]: [
|
||||
{ key: filterTypes.LESS_THAN, value: 'is before' },
|
||||
{ key: filterTypes.GREATER_THAN, value: 'is after' },
|
||||
{ key: filterTypes.IN_LAST, value: 'in the last' },
|
||||
{ key: filterTypes.NOT_IN_LAST, value: 'not in the last' },
|
||||
{ key: filterTypes.IN_NEXT, value: 'in the next' },
|
||||
{ key: filterTypes.NOT_IN_NEXT, value: 'not in the next' }
|
||||
{
|
||||
key: filterTypes.LESS_THAN,
|
||||
value: () => translate('FilterIsBefore')
|
||||
},
|
||||
{
|
||||
key: filterTypes.GREATER_THAN,
|
||||
value: () => translate('FilterIsAfter')
|
||||
},
|
||||
{
|
||||
key: filterTypes.IN_LAST,
|
||||
value: () => translate('FilterInLast')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_IN_LAST,
|
||||
value: () => translate('FilterNotInLast')
|
||||
},
|
||||
{
|
||||
key: filterTypes.IN_NEXT,
|
||||
value: () => translate('FilterInNext')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_IN_NEXT,
|
||||
value: () => translate('FilterNotInNext')
|
||||
}
|
||||
],
|
||||
|
||||
[EQUAL]: [
|
||||
{ key: filterTypes.EQUAL, value: 'is' }
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterIs')
|
||||
}
|
||||
],
|
||||
|
||||
[EXACT]: [
|
||||
{ key: filterTypes.EQUAL, value: 'is' },
|
||||
{ key: filterTypes.NOT_EQUAL, value: 'is not' }
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterIs')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_EQUAL,
|
||||
value: () => translate('FilterIsNot')
|
||||
}
|
||||
],
|
||||
|
||||
[NUMBER]: [
|
||||
{ key: filterTypes.EQUAL, value: 'equal' },
|
||||
{ key: filterTypes.GREATER_THAN, value: 'greater than' },
|
||||
{ key: filterTypes.GREATER_THAN_OR_EQUAL, value: 'greater than or equal' },
|
||||
{ key: filterTypes.LESS_THAN, value: 'less than' },
|
||||
{ key: filterTypes.LESS_THAN_OR_EQUAL, value: 'less than or equal' },
|
||||
{ key: filterTypes.NOT_EQUAL, value: 'not equal' }
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.GREATER_THAN,
|
||||
value: () => translate('FilterGreaterThan')
|
||||
},
|
||||
{
|
||||
key: filterTypes.GREATER_THAN_OR_EQUAL,
|
||||
value: () => translate('FilterGreaterThanOrEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.LESS_THAN,
|
||||
value: () => translate('FilterLessThan')
|
||||
},
|
||||
{
|
||||
key: filterTypes.LESS_THAN_OR_EQUAL,
|
||||
value: () => translate('FilterLessThanOrEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_EQUAL,
|
||||
value: () => translate('FilterNotEqual')
|
||||
}
|
||||
],
|
||||
|
||||
[STRING]: [
|
||||
{ key: filterTypes.CONTAINS, value: 'contains' },
|
||||
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' },
|
||||
{ key: filterTypes.EQUAL, value: 'equal' },
|
||||
{ key: filterTypes.NOT_EQUAL, value: 'not equal' },
|
||||
{ key: filterTypes.STARTS_WITH, value: 'starts with' },
|
||||
{ key: filterTypes.NOT_STARTS_WITH, value: 'does not start with' },
|
||||
{ key: filterTypes.ENDS_WITH, value: 'ends with' },
|
||||
{ key: filterTypes.NOT_ENDS_WITH, value: 'does not end with' }
|
||||
{
|
||||
key: filterTypes.CONTAINS,
|
||||
value: () => translate('FilterContains')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_CONTAINS,
|
||||
value: () => translate('FilterDoesNotContain')
|
||||
},
|
||||
{
|
||||
key: filterTypes.EQUAL,
|
||||
value: () => translate('FilterEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_EQUAL,
|
||||
value: () => translate('FilterNotEqual')
|
||||
},
|
||||
{
|
||||
key: filterTypes.STARTS_WITH,
|
||||
value: () => translate('FilterStartsWith')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_STARTS_WITH,
|
||||
value: () => translate('FilterDoesNotStartWith')
|
||||
},
|
||||
{
|
||||
key: filterTypes.ENDS_WITH,
|
||||
value: () => translate('FilterEndsWith')
|
||||
},
|
||||
{
|
||||
key: filterTypes.NOT_ENDS_WITH,
|
||||
value: () => translate('FilterDoesNotEndWith')
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ export const LANGUAGE = 'language';
|
||||
export const PROTOCOL = 'protocol';
|
||||
export const QUALITY = 'quality';
|
||||
export const QUALITY_PROFILE = 'qualityProfile';
|
||||
export const QUEUE_STATUS = 'queueStatus';
|
||||
export const MOVIE = 'movie';
|
||||
export const RELEASE_STATUS = 'releaseStatus';
|
||||
export const MINIMUM_AVAILABILITY = 'minimumAvailability';
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
faFileVideo as farFileVideo,
|
||||
faFolder as farFolder,
|
||||
faHdd as farHdd,
|
||||
faHeart as farHeart,
|
||||
faKeyboard as farKeyboard,
|
||||
faObjectGroup as farObjectGroup,
|
||||
faObjectUngroup as farObjectUngroup,
|
||||
@@ -174,6 +175,7 @@ export const GENRE = fasTheaterMasks;
|
||||
export const GROUP = farObjectGroup;
|
||||
export const HEALTH = fasMedkit;
|
||||
export const HEART = fasHeart;
|
||||
export const HEART_OUTLINE = farHeart;
|
||||
export const HISTORY = fasHistory;
|
||||
export const HOUSEKEEPING = fasHome;
|
||||
export const IGNORE = fasTimesCircle;
|
||||
|
||||
@@ -36,4 +36,5 @@ export type Kind =
|
||||
| 'primary'
|
||||
| 'purple'
|
||||
| 'success'
|
||||
| 'warning';
|
||||
| 'warning'
|
||||
| 'queue';
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.actions {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
}
|
||||
7
frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css.d.ts
vendored
Normal file
7
frontend/src/InteractiveImport/Folder/FavoriteFolderRow.css.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
48
frontend/src/InteractiveImport/Folder/FavoriteFolderRow.tsx
Normal file
48
frontend/src/InteractiveImport/Folder/FavoriteFolderRow.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import React, { SyntheticEvent, useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { removeFavoriteFolder } from 'Store/Actions/interactiveImportActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './FavoriteFolderRow.css';
|
||||
|
||||
interface FavoriteFolderRowProps {
|
||||
folder: string;
|
||||
onPress: (folder: string) => unknown;
|
||||
}
|
||||
|
||||
function FavoriteFolderRow({ folder, onPress }: FavoriteFolderRowProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
onPress(folder);
|
||||
}, [folder, onPress]);
|
||||
|
||||
const handleRemoveFavoritePress = useCallback(
|
||||
(e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
dispatch(removeFavoriteFolder({ folder }));
|
||||
},
|
||||
[folder, dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRowButton onPress={handlePress}>
|
||||
<TableRowCell>{folder}</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
title={translate('FavoriteFolderRemove')}
|
||||
kind="danger"
|
||||
name={icons.HEART}
|
||||
onPress={handleRemoveFavoritePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRowButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default FavoriteFolderRow;
|
||||
@@ -1,7 +1,12 @@
|
||||
.recentFoldersContainer {
|
||||
.foldersContainer {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.foldersTitle {
|
||||
border-bottom: 1px solid var(--borderColor);
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.buttonsContainer {
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ interface CssExports {
|
||||
'buttonContainer': string;
|
||||
'buttonIcon': string;
|
||||
'buttonsContainer': string;
|
||||
'recentFoldersContainer': string;
|
||||
'foldersContainer': string;
|
||||
'foldersTitle': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
@@ -14,14 +14,23 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import {
|
||||
addRecentFolder,
|
||||
removeRecentFolder,
|
||||
} from 'Store/Actions/interactiveImportActions';
|
||||
import { addRecentFolder } from 'Store/Actions/interactiveImportActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FavoriteFolderRow from './FavoriteFolderRow';
|
||||
import RecentFolderRow from './RecentFolderRow';
|
||||
import styles from './InteractiveImportSelectFolderModalContent.css';
|
||||
|
||||
const favoriteFoldersColumns = [
|
||||
{
|
||||
name: 'folder',
|
||||
label: () => translate('Folder'),
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: '',
|
||||
},
|
||||
];
|
||||
|
||||
const recentFoldersColumns = [
|
||||
{
|
||||
name: 'folder',
|
||||
@@ -49,15 +58,22 @@ function InteractiveImportSelectFolderModalContent(
|
||||
const { modalTitle, onFolderSelect, onModalClose } = props;
|
||||
const [folder, setFolder] = useState('');
|
||||
const dispatch = useDispatch();
|
||||
const recentFolders = useSelector(
|
||||
const { favoriteFolders, recentFolders } = useSelector(
|
||||
createSelector(
|
||||
(state: AppState) => state.interactiveImport.recentFolders,
|
||||
(recentFolders) => {
|
||||
return recentFolders;
|
||||
(state: AppState) => state.interactiveImport,
|
||||
(interactiveImport) => {
|
||||
return {
|
||||
favoriteFolders: interactiveImport.favoriteFolders,
|
||||
recentFolders: interactiveImport.recentFolders,
|
||||
};
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
const favoriteFolderMap = useMemo(() => {
|
||||
return new Map(favoriteFolders.map((f) => [f.folder, f]));
|
||||
}, [favoriteFolders]);
|
||||
|
||||
const onPathChange = useCallback(
|
||||
({ value }: { value: string }) => {
|
||||
setFolder(value);
|
||||
@@ -90,13 +106,6 @@ function InteractiveImportSelectFolderModalContent(
|
||||
onFolderSelect(folder);
|
||||
}, [folder, onFolderSelect, dispatch]);
|
||||
|
||||
const onRemoveRecentFolderPress = useCallback(
|
||||
(folderToRemove: string) => {
|
||||
dispatch(removeRecentFolder({ folder: folderToRemove }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
@@ -110,8 +119,34 @@ function InteractiveImportSelectFolderModalContent(
|
||||
onChange={onPathChange}
|
||||
/>
|
||||
|
||||
{favoriteFolders.length ? (
|
||||
<div className={styles.foldersContainer}>
|
||||
<div className={styles.foldersTitle}>
|
||||
{translate('FavoriteFolders')}
|
||||
</div>
|
||||
|
||||
<Table columns={favoriteFoldersColumns}>
|
||||
<TableBody>
|
||||
{favoriteFolders.map((favoriteFolder) => {
|
||||
return (
|
||||
<FavoriteFolderRow
|
||||
key={favoriteFolder.folder}
|
||||
folder={favoriteFolder.folder}
|
||||
onPress={onRecentPathPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{recentFolders.length ? (
|
||||
<div className={styles.recentFoldersContainer}>
|
||||
<div className={styles.foldersContainer}>
|
||||
<div className={styles.foldersTitle}>
|
||||
{translate('RecentFolders')}
|
||||
</div>
|
||||
|
||||
<Table columns={recentFoldersColumns}>
|
||||
<TableBody>
|
||||
{recentFolders
|
||||
@@ -123,8 +158,8 @@ function InteractiveImportSelectFolderModalContent(
|
||||
key={recentFolder.folder}
|
||||
folder={recentFolder.folder}
|
||||
lastUsed={recentFolder.lastUsed}
|
||||
isFavorite={favoriteFolderMap.has(recentFolder.folder)}
|
||||
onPress={onRecentPathPress}
|
||||
onRemoveRecentFolderPress={onRemoveRecentFolderPress}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
interface RecentFolder {
|
||||
folder: string;
|
||||
lastUsed: string;
|
||||
}
|
||||
|
||||
export default RecentFolder;
|
||||
@@ -1,5 +1,5 @@
|
||||
.actions {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 40px;
|
||||
width: 70px;
|
||||
}
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RecentFolderRow.css';
|
||||
|
||||
class RecentFolderRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.folder);
|
||||
};
|
||||
|
||||
onRemovePress = (event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
const {
|
||||
folder,
|
||||
onRemoveRecentFolderPress
|
||||
} = this.props;
|
||||
|
||||
onRemoveRecentFolderPress(folder);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
folder,
|
||||
lastUsed
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRowButton onPress={this.onPress}>
|
||||
<TableRowCell>{folder}</TableRowCell>
|
||||
|
||||
<RelativeDateCell date={lastUsed} />
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
title={translate('Remove')}
|
||||
name={icons.REMOVE}
|
||||
onPress={this.onRemovePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRowButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RecentFolderRow.propTypes = {
|
||||
folder: PropTypes.string.isRequired,
|
||||
lastUsed: PropTypes.string.isRequired,
|
||||
onPress: PropTypes.func.isRequired,
|
||||
onRemoveRecentFolderPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RecentFolderRow;
|
||||
85
frontend/src/InteractiveImport/Folder/RecentFolderRow.tsx
Normal file
85
frontend/src/InteractiveImport/Folder/RecentFolderRow.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { SyntheticEvent, useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import {
|
||||
addFavoriteFolder,
|
||||
removeFavoriteFolder,
|
||||
removeRecentFolder,
|
||||
} from 'Store/Actions/interactiveImportActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RecentFolderRow.css';
|
||||
|
||||
interface RecentFolderRowProps {
|
||||
folder: string;
|
||||
lastUsed: string;
|
||||
isFavorite: boolean;
|
||||
onPress: (folder: string) => unknown;
|
||||
}
|
||||
|
||||
function RecentFolderRow({
|
||||
folder,
|
||||
lastUsed,
|
||||
isFavorite,
|
||||
onPress,
|
||||
}: RecentFolderRowProps) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
onPress(folder);
|
||||
}, [folder, onPress]);
|
||||
|
||||
const handleFavoritePress = useCallback(
|
||||
(e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (isFavorite) {
|
||||
dispatch(removeFavoriteFolder({ folder }));
|
||||
} else {
|
||||
dispatch(addFavoriteFolder({ folder }));
|
||||
}
|
||||
},
|
||||
[folder, isFavorite, dispatch]
|
||||
);
|
||||
|
||||
const handleRemovePress = useCallback(
|
||||
(e: SyntheticEvent) => {
|
||||
e.stopPropagation();
|
||||
|
||||
dispatch(removeRecentFolder({ folder }));
|
||||
},
|
||||
[folder, dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<TableRowButton onPress={handlePress}>
|
||||
<TableRowCell>{folder}</TableRowCell>
|
||||
|
||||
<RelativeDateCell date={lastUsed} />
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
title={
|
||||
isFavorite
|
||||
? translate('FavoriteFolderRemove')
|
||||
: translate('FavoriteFolderAdd')
|
||||
}
|
||||
kind={isFavorite ? 'danger' : 'default'}
|
||||
name={isFavorite ? icons.HEART : icons.HEART_OUTLINE}
|
||||
onPress={handleFavoritePress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
title={translate('Remove')}
|
||||
name={icons.REMOVE}
|
||||
onPress={handleRemovePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRowButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default RecentFolderRow;
|
||||
@@ -192,10 +192,9 @@ const importModeSelector = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
interface InteractiveImportModalContentProps {
|
||||
export interface InteractiveImportModalContentProps {
|
||||
downloadId?: string;
|
||||
movieId?: number;
|
||||
seasonNumber?: number;
|
||||
showMovie?: boolean;
|
||||
allowMovieChange?: boolean;
|
||||
showDelete?: boolean;
|
||||
@@ -217,7 +216,6 @@ function InteractiveImportModalContent(
|
||||
const {
|
||||
downloadId,
|
||||
movieId,
|
||||
seasonNumber,
|
||||
allowMovieChange = true,
|
||||
showMovie = true,
|
||||
showFilterExistingFiles = false,
|
||||
@@ -343,7 +341,6 @@ function InteractiveImportModalContent(
|
||||
fetchInteractiveImportItems({
|
||||
downloadId,
|
||||
movieId,
|
||||
seasonNumber,
|
||||
folder,
|
||||
filterExistingFiles,
|
||||
})
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -4,9 +4,12 @@ import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import InteractiveImportSelectFolderModalContent from './Folder/InteractiveImportSelectFolderModalContent';
|
||||
import InteractiveImportModalContent from './Interactive/InteractiveImportModalContent';
|
||||
import InteractiveImportModalContent, {
|
||||
InteractiveImportModalContentProps,
|
||||
} from './Interactive/InteractiveImportModalContent';
|
||||
|
||||
interface InteractiveImportModalProps {
|
||||
interface InteractiveImportModalProps
|
||||
extends Omit<InteractiveImportModalContentProps, 'modalTitle'> {
|
||||
isOpen: boolean;
|
||||
folder?: string;
|
||||
downloadId?: string;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user