mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-05 13:21:25 -05:00
Compare commits
386 Commits
slack-refa
...
v4.2.3.657
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5616c5ba1 | ||
|
|
61979bff7a | ||
|
|
90d0d8bec8 | ||
|
|
2d814ecd20 | ||
|
|
6542119402 | ||
|
|
b9185574f3 | ||
|
|
99e0d42b71 | ||
|
|
d01fa5e6a4 | ||
|
|
2ce9d099e1 | ||
|
|
12829580e5 | ||
|
|
dadd796737 | ||
|
|
a3f508b8d4 | ||
|
|
1ab3df03a3 | ||
|
|
5558e10711 | ||
|
|
573405bae7 | ||
|
|
43d77308f9 | ||
|
|
b3c3f7ddae | ||
|
|
dd5bc41eda | ||
|
|
c8ab4f8c68 | ||
|
|
a4ddae0ccc | ||
|
|
d730161800 | ||
|
|
66c1af0555 | ||
|
|
dca00db317 | ||
|
|
812e5ac5a3 | ||
|
|
d01bae92bf | ||
|
|
1a6bf51741 | ||
|
|
f3e7843150 | ||
|
|
886b9b1c05 | ||
|
|
d8891ee4ea | ||
|
|
192dd9c137 | ||
|
|
b549fddf95 | ||
|
|
c1f538ed97 | ||
|
|
e72f8097fb | ||
|
|
3eec088306 | ||
|
|
ad097dd1a2 | ||
|
|
b4b38a5318 | ||
|
|
b0717a0803 | ||
|
|
4d1d08d345 | ||
|
|
e689817508 | ||
|
|
3b191caf16 | ||
|
|
cc6ca0b067 | ||
|
|
57cb63fb18 | ||
|
|
20f709d22a | ||
|
|
5d8775ac96 | ||
|
|
4890972e16 | ||
|
|
40dc808f61 | ||
|
|
97077e09d2 | ||
|
|
9ba7027d00 | ||
|
|
9903e70925 | ||
|
|
3a6f3666f5 | ||
|
|
915c66be50 | ||
|
|
70b22e483a | ||
|
|
cad1191da5 | ||
|
|
43910af127 | ||
|
|
f01c477b81 | ||
|
|
0054318658 | ||
|
|
03a3f4522a | ||
|
|
3d3562dcda | ||
|
|
7a079c5e0c | ||
|
|
4d70798f2f | ||
|
|
d55864f869 | ||
|
|
3c41c84fb0 | ||
|
|
eae9a6d6e0 | ||
|
|
867f8f5835 | ||
|
|
0c81387cfb | ||
|
|
c5fb5200de | ||
|
|
cc306fcd36 | ||
|
|
2bb7984961 | ||
|
|
21e605452a | ||
|
|
476f5b5bfd | ||
|
|
b6920cfe82 | ||
|
|
e89b98d0f6 | ||
|
|
1db690ad39 | ||
|
|
d5c524719b | ||
|
|
ced6586860 | ||
|
|
8b3019821a | ||
|
|
16ed68d5de | ||
|
|
098a893083 | ||
|
|
548e3400b5 | ||
|
|
5c31e3f1a2 | ||
|
|
7404793dcf | ||
|
|
d8af17ce3d | ||
|
|
44c912f02d | ||
|
|
b104368e23 | ||
|
|
aa0104b6bc | ||
|
|
69fcd8ec94 | ||
|
|
a59928c66a | ||
|
|
1cb7ae11a2 | ||
|
|
ca519047dd | ||
|
|
f15a6abde0 | ||
|
|
2aacebc938 | ||
|
|
120e9b673e | ||
|
|
0a77a13fa8 | ||
|
|
383f9647c3 | ||
|
|
7f7c672b93 | ||
|
|
2690ad8fe1 | ||
|
|
801204b6de | ||
|
|
cb9514abaf | ||
|
|
fd22cb44f6 | ||
|
|
2d68716376 | ||
|
|
b97e76c8b8 | ||
|
|
bfad4a8cd1 | ||
|
|
61f05710f5 | ||
|
|
a8ecefd91f | ||
|
|
e3468daba0 | ||
|
|
f2a7d0d520 | ||
|
|
43257f0726 | ||
|
|
6c2bf860fe | ||
|
|
3a1d848e59 | ||
|
|
f6590e71d2 | ||
|
|
586dd737fd | ||
|
|
fa84dda38c | ||
|
|
4a233ce915 | ||
|
|
ffdd9a1708 | ||
|
|
f890aadffa | ||
|
|
a8695959f1 | ||
|
|
696bb845a5 | ||
|
|
301a6904c0 | ||
|
|
8b4621db61 | ||
|
|
822b597f26 | ||
|
|
737a0176d4 | ||
|
|
5dc541c69e | ||
|
|
a9627771e6 | ||
|
|
5facab0744 | ||
|
|
63dde3eb89 | ||
|
|
144f564076 | ||
|
|
a6205c1ad4 | ||
|
|
64f27bca4f | ||
|
|
8a84975954 | ||
|
|
e923b2fc6c | ||
|
|
a4136150d0 | ||
|
|
1f1f3cdaa2 | ||
|
|
bd85936a62 | ||
|
|
93b2395228 | ||
|
|
14cccd3a23 | ||
|
|
006c9289de | ||
|
|
79cd2b2346 | ||
|
|
f80272a659 | ||
|
|
a158e008e9 | ||
|
|
c798987379 | ||
|
|
097b46c49f | ||
|
|
ddeb3a7840 | ||
|
|
50018d0325 | ||
|
|
a37fc0dc1f | ||
|
|
39ad315e73 | ||
|
|
0559996566 | ||
|
|
e7d4429fe4 | ||
|
|
6c494e9a92 | ||
|
|
62faa1aad8 | ||
|
|
907950e309 | ||
|
|
1caced614e | ||
|
|
5824ba963b | ||
|
|
7f2d5d8d10 | ||
|
|
81bffe243a | ||
|
|
2d6fde282a | ||
|
|
3125b038d5 | ||
|
|
89e25a6241 | ||
|
|
4db6688fe0 | ||
|
|
4ac1aeaf06 | ||
|
|
e2ae743ee1 | ||
|
|
9ad316a6f5 | ||
|
|
b643d2e23d | ||
|
|
6a03eddda9 | ||
|
|
1576bf1f17 | ||
|
|
6325b70e27 | ||
|
|
24206ad0a3 | ||
|
|
2fc7cbff89 | ||
|
|
55ef505d74 | ||
|
|
cabdad6306 | ||
|
|
8d4b2dd21b | ||
|
|
ad04031c99 | ||
|
|
e9a5f87e45 | ||
|
|
bc6ac0cd4b | ||
|
|
c2328e4b79 | ||
|
|
36119facf0 | ||
|
|
a1fa1ddf5d | ||
|
|
ba770dce73 | ||
|
|
d58f0806f6 | ||
|
|
a95f8fa873 | ||
|
|
d1a9cf98cc | ||
|
|
be29fc6adb | ||
|
|
c1085e965b | ||
|
|
dc5c997e9b | ||
|
|
040acbf65a | ||
|
|
d089d036e6 | ||
|
|
46732c7d73 | ||
|
|
8fd3254745 | ||
|
|
9b21408a03 | ||
|
|
bd53092f0c | ||
|
|
80b1aa9a2c | ||
|
|
df863a08a1 | ||
|
|
8a0c318540 | ||
|
|
4f9d067361 | ||
|
|
6739310ba8 | ||
|
|
a45b91abe8 | ||
|
|
1ad2dc54b3 | ||
|
|
7f0b708cb9 | ||
|
|
c8cdf03077 | ||
|
|
c8afe578f3 | ||
|
|
9bb394f420 | ||
|
|
4f512c5cdf | ||
|
|
8ae84222d1 | ||
|
|
7ec0fd1cea | ||
|
|
ad8629fac9 | ||
|
|
32393eabb7 | ||
|
|
6f42dd671f | ||
|
|
966963b53d | ||
|
|
43a74ab32a | ||
|
|
897e4b4d0a | ||
|
|
0dd639ff76 | ||
|
|
64da17a9ab | ||
|
|
dbaabf90a5 | ||
|
|
1aabc4bc5c | ||
|
|
cce4334310 | ||
|
|
8d54b4268b | ||
|
|
2be9b306a3 | ||
|
|
7f5cd9a765 | ||
|
|
d26ca9a7a5 | ||
|
|
933796e214 | ||
|
|
121a347fc9 | ||
|
|
ed060d552f | ||
|
|
51485259ae | ||
|
|
76a3ca37ed | ||
|
|
e5e7292f11 | ||
|
|
397ee20160 | ||
|
|
2914c3c88f | ||
|
|
750cb94692 | ||
|
|
1ffbae9d16 | ||
|
|
a515c5c7a5 | ||
|
|
c58f31d690 | ||
|
|
2041797587 | ||
|
|
b1d8763593 | ||
|
|
ff7f95a0ab | ||
|
|
ccd561eb24 | ||
|
|
3e353a6710 | ||
|
|
544e942fe4 | ||
|
|
8bb3764708 | ||
|
|
356013a84e | ||
|
|
75b2414e29 | ||
|
|
4084ce5c2f | ||
|
|
ca2f12338e | ||
|
|
347fdc1572 | ||
|
|
6c04d0701c | ||
|
|
3f4e5e55a3 | ||
|
|
7310481e6a | ||
|
|
e59a1a64f9 | ||
|
|
5c1ebc72b3 | ||
|
|
a96971beff | ||
|
|
b34f4fde1b | ||
|
|
a9b2635cb0 | ||
|
|
1ed8bd7350 | ||
|
|
f0891e9df7 | ||
|
|
5834afe30b | ||
|
|
c015a3ebf0 | ||
|
|
8fd1f121f4 | ||
|
|
b18cc49a83 | ||
|
|
69b969cfc8 | ||
|
|
bc3e3714b9 | ||
|
|
85dd5f5754 | ||
|
|
7de270b212 | ||
|
|
b1afd7aaaa | ||
|
|
894fafcad7 | ||
|
|
2cf62915b0 | ||
|
|
d3743446da | ||
|
|
b332fa55de | ||
|
|
36da57f87b | ||
|
|
07bb5e416b | ||
|
|
69207ba77b | ||
|
|
ff409d3661 | ||
|
|
dfb8d2ea0f | ||
|
|
603db7c76b | ||
|
|
6fa0cdc9a8 | ||
|
|
c0cbbc7ed4 | ||
|
|
daa9ee30a2 | ||
|
|
86102349c5 | ||
|
|
c4d035f0ad | ||
|
|
95d44f968f | ||
|
|
e7a8f6332c | ||
|
|
b8c92d23f4 | ||
|
|
093e076db0 | ||
|
|
f6f949415c | ||
|
|
ea2576a56c | ||
|
|
595acb696d | ||
|
|
38c9534eac | ||
|
|
9377ef7942 | ||
|
|
c2e5686bcf | ||
|
|
f08807daf6 | ||
|
|
72b3caa72d | ||
|
|
589368781b | ||
|
|
8fd6101121 | ||
|
|
ac9d6cbf0a | ||
|
|
6e0ed36e9f | ||
|
|
fcb65055ef | ||
|
|
90456bbfed | ||
|
|
2a74b7b2e1 | ||
|
|
fc08c39fb8 | ||
|
|
76d65bf990 | ||
|
|
de243991dd | ||
|
|
4d1f251c1f | ||
|
|
ebb1e3131a | ||
|
|
6e502d63c2 | ||
|
|
57e05b70da | ||
|
|
59186adbfc | ||
|
|
bc20e159ba | ||
|
|
39b99341cd | ||
|
|
b626c5bbf0 | ||
|
|
a33b861cec | ||
|
|
3a48f07702 | ||
|
|
1aec0b7ee5 | ||
|
|
3e32161791 | ||
|
|
fda1ad237b | ||
|
|
52b6f39026 | ||
|
|
100fd95dd9 | ||
|
|
d571c7b75a | ||
|
|
8d7f48739b | ||
|
|
c061d7cec8 | ||
|
|
91691205db | ||
|
|
c1e07b30d7 | ||
|
|
78a7770858 | ||
|
|
599f4907f4 | ||
|
|
ec9a7f5c8e | ||
|
|
54c914d48f | ||
|
|
75270d8151 | ||
|
|
7a859f340b | ||
|
|
13e44ce19a | ||
|
|
9e4c94592d | ||
|
|
9d2a59b7fd | ||
|
|
194e0f3d7f | ||
|
|
d1fa92bc6c | ||
|
|
974e44ce48 | ||
|
|
de05be62d7 | ||
|
|
cae5badee0 | ||
|
|
45d8227654 | ||
|
|
7bbd2246c4 | ||
|
|
59fed13442 | ||
|
|
50b273acae | ||
|
|
4278415fd7 | ||
|
|
124b50288d | ||
|
|
3fcc395964 | ||
|
|
0ee9981cba | ||
|
|
2848899206 | ||
|
|
f1a00764cd | ||
|
|
346236764c | ||
|
|
eecd4e4b7d | ||
|
|
2838d8ca29 | ||
|
|
4ebcbc28aa | ||
|
|
2c24f7ca04 | ||
|
|
ec86de78d2 | ||
|
|
4f5f9ff77e | ||
|
|
465bb403a9 | ||
|
|
9e175e28ef | ||
|
|
4d2a311e40 | ||
|
|
b2195148a2 | ||
|
|
2ae7371d73 | ||
|
|
7b03a856c9 | ||
|
|
48d1d47b67 | ||
|
|
906b9bb92a | ||
|
|
6fc14278e6 | ||
|
|
34b269086d | ||
|
|
6c40a27f2e | ||
|
|
be158a09b4 | ||
|
|
eecd746f51 | ||
|
|
5ed034320e | ||
|
|
41dd678dfd | ||
|
|
716eadc551 | ||
|
|
1cb31aa95c | ||
|
|
568dd2fbb2 | ||
|
|
5d091e519e | ||
|
|
faab78c00a | ||
|
|
f1461056ce | ||
|
|
2042ffce62 | ||
|
|
c6ae6f7b1c | ||
|
|
8d7affae68 | ||
|
|
a418111245 | ||
|
|
6359ed5757 | ||
|
|
9a8c1d7d1b | ||
|
|
e89c2ee9f7 | ||
|
|
3d36f88939 | ||
|
|
8e4320a93b | ||
|
|
b095676010 | ||
|
|
41d69d8484 | ||
|
|
073e59e3db | ||
|
|
588a0843a4 | ||
|
|
26cedfd47d | ||
|
|
16789e5b6b | ||
|
|
159edcde94 |
@@ -19,10 +19,10 @@ indent_size = 4
|
||||
dotnet_sort_system_directives_first = true
|
||||
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:refactoring
|
||||
dotnet_style_qualification_for_property = false:refactoring
|
||||
dotnet_style_qualification_for_method = false:refactoring
|
||||
dotnet_style_qualification_for_event = false:refactoring
|
||||
dotnet_style_qualification_for_field = false:warning
|
||||
dotnet_style_qualification_for_property = false:warning
|
||||
dotnet_style_qualification_for_method = false:warning
|
||||
dotnet_style_qualification_for_event = false:warning
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
@@ -32,10 +32,6 @@ csharp_indent_case_contents_when_block = true
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = flush_left
|
||||
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
dotnet_naming_style.instance_field_style.capitalization = camel_case
|
||||
dotnet_naming_style.instance_field_style.required_prefix = _
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"paths": [
|
||||
"frontend/src/**/*.js",
|
||||
"src/NzbDrone.Core/Localization/Core/*.json"
|
||||
"frontend/src/**/*.js"
|
||||
],
|
||||
"ignored": [
|
||||
"**/node_modules/**/*"
|
||||
|
||||
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
6
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -5,9 +5,9 @@ body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Is there an existing issue for this?
|
||||
description: Please search to see if an issue already exists for the bug you encountered.
|
||||
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
|
||||
options:
|
||||
- label: I have searched the existing issues
|
||||
- label: I have searched the existing open and closed issues
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
@@ -42,12 +42,14 @@ body:
|
||||
- **Docker Install**: Yes
|
||||
- **Using Reverse Proxy**: No
|
||||
- **Browser**: Firefox 90 (If UI related)
|
||||
- **Database**: Sqlite 3.36.0
|
||||
value: |
|
||||
- OS:
|
||||
- Radarr:
|
||||
- Docker Install:
|
||||
- Using Reverse Proxy:
|
||||
- Browser:
|
||||
- Database:
|
||||
render: markdown
|
||||
validations:
|
||||
required: true
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -187,6 +187,10 @@ packages.config.md5sum
|
||||
**/.idea/**/*.iml
|
||||
**/.idea/**/contentModel.xml
|
||||
**/.idea/**/modules.xml
|
||||
|
||||
# ignore node_modules symlink
|
||||
node_modules
|
||||
node_modules.nosync
|
||||
|
||||
# API doc generation
|
||||
.config/
|
||||
|
||||
@@ -7,14 +7,20 @@ variables:
|
||||
outputFolder: './_output'
|
||||
artifactsFolder: './_artifacts'
|
||||
testsFolder: './_tests'
|
||||
majorVersion: '4.0.0'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '4.2.3'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.100'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
dotnetVersion: '6.0.400'
|
||||
nodeVersion: '16.X'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-11'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
@@ -29,6 +35,7 @@ pr:
|
||||
paths:
|
||||
exclude:
|
||||
- src/NzbDrone.Core/Localization/Core
|
||||
- src/Radarr.Api.*/openapi.json
|
||||
|
||||
stages:
|
||||
- stage: Setup
|
||||
@@ -37,7 +44,7 @@ stages:
|
||||
- job:
|
||||
displayName: Build Variables
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
# Set the build name properly. The 'name' property won't recursively expand so hack here:
|
||||
- bash: echo "##vso[build.updatebuildnumber]$RADARRVERSION"
|
||||
@@ -63,15 +70,15 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
enableAnalysis: 'true'
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.15'
|
||||
imageName: ${{ variables.macImage }}
|
||||
enableAnalysis: 'false'
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
enableAnalysis: 'false'
|
||||
|
||||
pool:
|
||||
@@ -90,15 +97,14 @@ stages:
|
||||
- bash: |
|
||||
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
|
||||
echo $BUNDLEDVERSIONS
|
||||
grep osx-x64 $BUNDLEDVERSIONS
|
||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
echo "BSD already enabled"
|
||||
echo "Extra platforms already enabled"
|
||||
else
|
||||
echo "Enabling BSD support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
|
||||
echo "Enabling extra platform support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
displayName: Enable FreeBSD Support
|
||||
- bash: ./build.sh --backend --enable-bsd
|
||||
displayName: Enable Extra Platform Support
|
||||
- bash: ./build.sh --backend --enable-extra-platforms
|
||||
displayName: Build Radarr Backend
|
||||
- bash: |
|
||||
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
|
||||
@@ -112,24 +118,28 @@ stages:
|
||||
displayName: Publish Backend
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
||||
artifact: WindowsCoreTests
|
||||
displayName: Publish Windows Test Package
|
||||
artifact: win-x64-tests
|
||||
displayName: Publish win-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
||||
artifact: LinuxCoreTests
|
||||
displayName: Publish Linux Test Package
|
||||
artifact: linux-x64-tests
|
||||
displayName: Publish linux-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
|
||||
artifact: linux-x86-tests
|
||||
displayName: Publish linux-x86 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
||||
artifact: LinuxMuslCoreTests
|
||||
displayName: Publish Linux Musl Test Package
|
||||
artifact: linux-musl-x64-tests
|
||||
displayName: Publish linux-musl-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
||||
artifact: FreebsdCoreTests
|
||||
displayName: Publish FreeBSD Test Package
|
||||
artifact: freebsd-x64-tests
|
||||
displayName: Publish freebsd-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
||||
artifact: MacCoreTests
|
||||
displayName: Publish MacOS Test Package
|
||||
artifact: osx-x64-tests
|
||||
displayName: Publish osx-x64 Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
|
||||
- stage: Build_Frontend
|
||||
@@ -141,20 +151,20 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.15'
|
||||
imageName: ${{ variables.macImage }}
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
versionSpec: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -163,7 +173,6 @@ stages:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --frontend
|
||||
@@ -184,7 +193,7 @@ stages:
|
||||
- job: Windows_Installer
|
||||
displayName: Create Installer
|
||||
pool:
|
||||
vmImage: 'windows-2019'
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 1
|
||||
@@ -200,16 +209,11 @@ stages:
|
||||
artifactName: WindowsFrontend
|
||||
targetPath: _output
|
||||
displayName: Fetch Frontend
|
||||
- bash: ./build.sh --packages
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net6.0 //DRuntime=win-x86
|
||||
cp setup/output/Radarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create .NET Core Windows installer
|
||||
- bash: |
|
||||
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net6.0 //DRuntime=win-x64
|
||||
cp setup/output/Radarr.*windows.net6.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
displayName: Create .NET Core Windows installer
|
||||
./build.sh --packages --installer
|
||||
cp setup/output/Radarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
cp setup/output/Radarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create Installers
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'WindowsInstaller'
|
||||
displayName: Publish Installer
|
||||
@@ -222,7 +226,7 @@ stages:
|
||||
- job: Other_Packages
|
||||
displayName: Create Standard Packages
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 1
|
||||
@@ -238,7 +242,7 @@ stages:
|
||||
artifactName: WindowsFrontend
|
||||
targetPath: _output
|
||||
displayName: Fetch Frontend
|
||||
- bash: ./build.sh --packages --enable-bsd
|
||||
- bash: ./build.sh --packages --enable-extra-platforms
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
find . -name "ffprobe" -exec chmod a+x {} \;
|
||||
@@ -246,28 +250,28 @@ stages:
|
||||
find . -name "Radarr.Update" -exec chmod a+x {} \;
|
||||
displayName: Set executable bits
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Windows Core zip
|
||||
displayName: Create win-x64 zip
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Windows x86 Core zip
|
||||
displayName: Create win-x86 zip
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS x64 Core app
|
||||
displayName: Create osx-x64 app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS x64 Core tar
|
||||
displayName: Create osx-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -275,14 +279,14 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS arm64 Core app
|
||||
displayName: Create osx-arm64 app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-arm64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS arm64 Core tar
|
||||
displayName: Create osx-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -290,7 +294,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Core tar
|
||||
displayName: Create linux-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -298,7 +302,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Musl Core tar
|
||||
displayName: Create linux-musl-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -306,7 +310,15 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM32 Linux Core tar
|
||||
displayName: Create linux-x86 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-x86.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-arm tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -314,7 +326,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM32 Linux Musl Core tar
|
||||
displayName: Create linux-musl-arm tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -322,7 +334,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM64 Linux Core tar
|
||||
displayName: Create linux-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -330,7 +342,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM64 Linux Musl Core tar
|
||||
displayName: Create linux-musl-arm64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -338,7 +350,7 @@ stages:
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create FreeBSD Core Core tar
|
||||
displayName: Create freebsd-x64 tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).freebsd-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
@@ -383,7 +395,7 @@ stages:
|
||||
jobs:
|
||||
- job: Prepare
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
@@ -405,22 +417,22 @@ stages:
|
||||
matrix:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
testName: 'osx-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: 'macos-10.15'
|
||||
imageName: ${{ variables.macImage }}
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
testName: 'win-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: 'windows-2019'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
LinuxCore:
|
||||
osName: 'Linux'
|
||||
testName: 'LinuxCore'
|
||||
testName: 'linux-x64'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: 'ubuntu-18.04'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
FreebsdCore:
|
||||
osName: 'Linux'
|
||||
testName: 'FreebsdCore'
|
||||
testName: 'freebsd-x64'
|
||||
poolName: 'FreeBSD'
|
||||
imageName:
|
||||
|
||||
@@ -439,7 +451,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(testName)Tests'
|
||||
artifactName: '$(testName)-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
@@ -473,11 +485,15 @@ stages:
|
||||
matrix:
|
||||
alpine:
|
||||
testName: 'Musl Net Core'
|
||||
artifactName: LinuxMuslCoreTests
|
||||
artifactName: linux-musl-x64-tests
|
||||
containerImage: ghcr.io/servarr/testimages:alpine
|
||||
linux-x86:
|
||||
testName: 'linux-x86'
|
||||
artifactName: linux-x86-tests
|
||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
container: $[ variables['containerImage'] ]
|
||||
|
||||
@@ -485,9 +501,15 @@ stages:
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
displayName: 'Install .NET'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
||||
- bash: |
|
||||
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
||||
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
||||
displayName: 'Install .NET'
|
||||
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
@@ -513,6 +535,62 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- job: Unit_LinuxCore_Postgres
|
||||
displayName: Unit Native LinuxCore with Postgres Database
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
variables:
|
||||
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||
artifactName: linux-x64-tests
|
||||
Radarr__Postgres__Host: 'localhost'
|
||||
Radarr__Postgres__Port: '5432'
|
||||
Radarr__Postgres__User: 'radarr'
|
||||
Radarr__Postgres__Password: 'radarr'
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
timeoutInMinutes: 10
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: $(artifactName)
|
||||
targetPath: $(testsFolder)
|
||||
- bash: |
|
||||
chmod a+x _tests/ffprobe
|
||||
displayName: Make ffprobe Executable
|
||||
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
|
||||
displayName: Make Test Dummy Executable
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
- bash: |
|
||||
docker run -d --name=postgres14 \
|
||||
-e POSTGRES_PASSWORD=radarr \
|
||||
-e POSTGRES_USER=radarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
ls -lR ${TESTSFOLDER}
|
||||
${TESTSFOLDER}/test.sh Linux Unit Test
|
||||
displayName: Run Tests
|
||||
- task: PublishTestResults@2
|
||||
displayName: Publish Test Results
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'LinuxCore Postgres Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- stage: Integration
|
||||
displayName: Integration
|
||||
@@ -521,7 +599,7 @@ stages:
|
||||
jobs:
|
||||
- job: Prepare
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
@@ -540,18 +618,18 @@ stages:
|
||||
matrix:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
imageName: 'macos-10.15'
|
||||
testName: 'osx-x64'
|
||||
imageName: ${{ variables.macImage }}
|
||||
pattern: 'Radarr.*.osx-core-x64.tar.gz'
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
imageName: 'windows-2019'
|
||||
testName: 'win-x64'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pattern: 'Radarr.*.windows-core-x64.zip'
|
||||
LinuxCore:
|
||||
osName: 'Linux'
|
||||
testName: 'LinuxCore'
|
||||
imageName: 'ubuntu-18.04'
|
||||
testName: 'linux-x64'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||
|
||||
pool:
|
||||
@@ -567,7 +645,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(testName)Tests'
|
||||
artifactName: '$(testName)-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -597,6 +675,68 @@ stages:
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_LinuxCore_Postgres
|
||||
displayName: Integration Native LinuxCore with Postgres Database
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
variables:
|
||||
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||
Radarr__Postgres__Host: 'localhost'
|
||||
Radarr__Postgres__Port: '5432'
|
||||
Radarr__Postgres__User: 'radarr'
|
||||
Radarr__Postgres__Password: 'radarr'
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'linux-x64-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: Packages
|
||||
itemPattern: '**/$(pattern)'
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- task: ExtractFiles@1
|
||||
inputs:
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||
displayName: Extract Package
|
||||
- bash: |
|
||||
mkdir -p ./bin/
|
||||
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/
|
||||
displayName: Move Package Contents
|
||||
- bash: |
|
||||
docker run -d --name=postgres14 \
|
||||
-e POSTGRES_PASSWORD=radarr \
|
||||
-e POSTGRES_USER=radarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
${TESTSFOLDER}/test.sh Linux Integration Test
|
||||
displayName: Run Integration Tests
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_FreeBSD
|
||||
displayName: Integration Native FreeBSD
|
||||
dependsOn: Prepare
|
||||
@@ -614,7 +754,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'FreebsdCoreTests'
|
||||
artifactName: 'freebsd-x64-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -650,12 +790,17 @@ stages:
|
||||
strategy:
|
||||
matrix:
|
||||
alpine:
|
||||
testName: 'Musl Net Core'
|
||||
artifactName: LinuxMuslCoreTests
|
||||
testName: 'linux-musl-x64'
|
||||
artifactName: linux-musl-x64-tests
|
||||
containerImage: ghcr.io/servarr/testimages:alpine
|
||||
pattern: 'Radarr.*.linux-musl-core-x64.tar.gz'
|
||||
linux-x86:
|
||||
testName: 'linux-x86'
|
||||
artifactName: linux-x86-tests
|
||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
||||
pattern: 'Radarr.*.linux-core-x86.tar.gz'
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
|
||||
container: $[ variables['containerImage'] ]
|
||||
|
||||
@@ -663,9 +808,15 @@ stages:
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
displayName: 'Install .NET'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
|
||||
- bash: |
|
||||
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
|
||||
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
|
||||
displayName: 'Install .NET'
|
||||
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
@@ -711,17 +862,20 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
artifactName: 'linux-x64'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
pattern: 'Radarr.*.linux-core-x64.tar.gz'
|
||||
failBuild: true
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.15'
|
||||
artifactName: 'osx-x64'
|
||||
imageName: ${{ variables.macImage }}
|
||||
pattern: 'Radarr.*.osx-core-x64.tar.gz'
|
||||
failBuild: true
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
artifactName: 'win-x64'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pattern: 'Radarr.*.windows-core-x64.zip'
|
||||
failBuild: true
|
||||
|
||||
@@ -738,7 +892,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(osName)CoreTests'
|
||||
artifactName: '$(artifactName)-tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -787,7 +941,7 @@ stages:
|
||||
jobs:
|
||||
- job: Prepare
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
@@ -804,17 +958,17 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: '12.x'
|
||||
versionSpec: $(nodeVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
@@ -823,7 +977,6 @@ stages:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --lint
|
||||
@@ -836,7 +989,7 @@ stages:
|
||||
displayName: Frontend
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
pool:
|
||||
vmImage: windows-2019
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
steps:
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
- task: SonarCloudPrepare@1
|
||||
@@ -852,6 +1005,60 @@ stages:
|
||||
cliProjectVersion: '$(radarrVersion)'
|
||||
cliSources: './frontend'
|
||||
- task: SonarCloudAnalyze@1
|
||||
|
||||
- job: Api_Docs
|
||||
displayName: API Docs
|
||||
dependsOn: Prepare
|
||||
condition: |
|
||||
and
|
||||
(
|
||||
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop')),
|
||||
and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
)
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: self
|
||||
submodules: true
|
||||
persistCredentials: true
|
||||
fetchDepth: 1
|
||||
- bash: ./docs.sh Windows
|
||||
displayName: Create openapi.json
|
||||
- bash: |
|
||||
git config --global user.email "development@lidarr.audio"
|
||||
git config --global user.name "Servarr"
|
||||
git checkout -b api-docs
|
||||
git add .
|
||||
git status
|
||||
if git status | grep modified
|
||||
then
|
||||
git commit -am 'Automated API Docs update'
|
||||
git push -f --set-upstream origin api-docs
|
||||
curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/radarr/radarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
|
||||
else
|
||||
echo "No changes since last run"
|
||||
fi
|
||||
displayName: Commit API Doc Change
|
||||
continueOnError: true
|
||||
env:
|
||||
GITHUBTOKEN: $(githubToken)
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Copy openapi.json to: $(Build.ArtifactStagingDirectory)'
|
||||
inputs:
|
||||
SourceFolder: '$(Build.SourcesDirectory)'
|
||||
Contents: |
|
||||
**/*openapi.json
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)/api_docs'
|
||||
- publish: $(Build.ArtifactStagingDirectory)/api_docs
|
||||
artifact: 'APIDocs'
|
||||
displayName: Publish API Docs Bundle
|
||||
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
|
||||
|
||||
- job: Analyze_Backend
|
||||
displayName: Backend
|
||||
@@ -863,7 +1070,7 @@ stages:
|
||||
EnableAnalyzers: 'false'
|
||||
|
||||
pool:
|
||||
vmImage: windows-2019
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
@@ -920,7 +1127,7 @@ stages:
|
||||
- job:
|
||||
displayName: Discord Notification
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
continueOnError: true
|
||||
|
||||
83
build.sh
83
build.sh
@@ -25,14 +25,22 @@ UpdateVersionNumber()
|
||||
fi
|
||||
}
|
||||
|
||||
EnableBsdSupport()
|
||||
EnableExtraPlatformsInSDK()
|
||||
{
|
||||
#todo enable sdk with
|
||||
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
|
||||
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
|
||||
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
echo "Extra platforms already enabled"
|
||||
else
|
||||
echo "Enabling extra platform support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
}
|
||||
|
||||
EnableExtraPlatforms()
|
||||
{
|
||||
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -233,6 +241,32 @@ Package()
|
||||
esac
|
||||
}
|
||||
|
||||
BuildInstaller()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
./_inno/ISCC.exe setup/radarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
|
||||
}
|
||||
|
||||
InstallInno()
|
||||
{
|
||||
ProgressStart "Installing portable Inno Setup"
|
||||
|
||||
rm -rf _inno
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
|
||||
mkdir _inno
|
||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||
rm innosetup.exe
|
||||
|
||||
ProgressEnd "Installed portable Inno Setup"
|
||||
}
|
||||
|
||||
RemoveInno()
|
||||
{
|
||||
rm -rf _inno
|
||||
}
|
||||
|
||||
PackageTests()
|
||||
{
|
||||
local framework="$1"
|
||||
@@ -264,8 +298,10 @@ if [ $# -eq 0 ]; then
|
||||
BACKEND=YES
|
||||
FRONTEND=YES
|
||||
PACKAGES=YES
|
||||
INSTALLER=NO
|
||||
LINT=YES
|
||||
ENABLE_BSD=NO
|
||||
ENABLE_EXTRA_PLATFORMS=NO
|
||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
@@ -277,8 +313,12 @@ case $key in
|
||||
BACKEND=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--enable-bsd)
|
||||
ENABLE_BSD=YES
|
||||
--enable-bsd|--enable-extra-platforms)
|
||||
ENABLE_EXTRA_PLATFORMS=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--enable-extra-platforms-in-sdk)
|
||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
|
||||
shift # past argument
|
||||
;;
|
||||
-r|--runtime)
|
||||
@@ -299,6 +339,10 @@ case $key in
|
||||
PACKAGES=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--installer)
|
||||
INSTALLER=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--lint)
|
||||
LINT=YES
|
||||
shift # past argument
|
||||
@@ -318,12 +362,17 @@ esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]}" # restore positional parameters
|
||||
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
|
||||
then
|
||||
EnableExtraPlatformsInSDK
|
||||
fi
|
||||
|
||||
if [ "$BACKEND" = "YES" ];
|
||||
then
|
||||
UpdateVersionNumber
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
EnableBsdSupport
|
||||
EnableExtraPlatforms
|
||||
fi
|
||||
Build
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
@@ -333,9 +382,10 @@ then
|
||||
PackageTests "net6.0" "linux-x64"
|
||||
PackageTests "net6.0" "linux-musl-x64"
|
||||
PackageTests "net6.0" "osx-x64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
PackageTests "net6.0" "freebsd-x64"
|
||||
PackageTests "net6.0" "linux-x86"
|
||||
fi
|
||||
else
|
||||
PackageTests "$FRAMEWORK" "$RID"
|
||||
@@ -374,11 +424,20 @@ then
|
||||
Package "net6.0" "linux-musl-arm"
|
||||
Package "net6.0" "osx-x64"
|
||||
Package "net6.0" "osx-arm64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
then
|
||||
Package "net6.0" "freebsd-x64"
|
||||
Package "net6.0" "linux-x86"
|
||||
fi
|
||||
else
|
||||
Package "$FRAMEWORK" "$RID"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$INSTALLER" = "YES" ];
|
||||
then
|
||||
InstallInno
|
||||
BuildInstaller "net6.0" "win-x64"
|
||||
BuildInstaller "net6.0" "win-x86"
|
||||
RemoveInno
|
||||
fi
|
||||
|
||||
38
docs.sh
Normal file
38
docs.sh
Normal file
@@ -0,0 +1,38 @@
|
||||
PLATFORM=$1
|
||||
|
||||
if [ "$PLATFORM" = "Windows" ]; then
|
||||
RUNTIME="win-x64"
|
||||
elif [ "$PLATFORM" = "Linux" ]; then
|
||||
WHERE="linux-x64"
|
||||
elif [ "$PLATFORM" = "Mac" ]; then
|
||||
WHERE="osx-x64"
|
||||
else
|
||||
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
outputFolder='_output'
|
||||
testPackageFolder='_tests'
|
||||
|
||||
rm -rf $outputFolder
|
||||
rm -rf $testPackageFolder
|
||||
|
||||
slnFile=src/Radarr.sln
|
||||
|
||||
platform=Posix
|
||||
|
||||
dotnet clean $slnFile -c Debug
|
||||
dotnet clean $slnFile -c Release
|
||||
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||
|
||||
dotnet new tool-manifest
|
||||
dotnet tool install --version 6.3.0 Swashbuckle.AspNetCore.Cli
|
||||
|
||||
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v3 &
|
||||
|
||||
sleep 45
|
||||
|
||||
kill %1
|
||||
|
||||
exit 0
|
||||
@@ -223,7 +223,7 @@ module.exports = (env) => {
|
||||
{
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10240,
|
||||
limit: 24096,
|
||||
mimetype: 'application/font-woff',
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
@@ -233,11 +233,12 @@ module.exports = (env) => {
|
||||
},
|
||||
|
||||
{
|
||||
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
test: /\.(ttf|eot|eot?#iefix|gif|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
limit: 24096,
|
||||
emitFile: false,
|
||||
name: 'Content/Fonts/[name].[ext]'
|
||||
}
|
||||
|
||||
@@ -61,33 +61,33 @@ class Blocklist extends Component {
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedConfirmed = () => {
|
||||
this.props.onRemoveSelected(this.getSelectedIds());
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -120,7 +120,7 @@ class Blocklist extends Component {
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Remove Selected"
|
||||
label={translate('RemoveSelected')}
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={!selectedIds.length}
|
||||
isSpinning={isRemoving}
|
||||
|
||||
@@ -65,37 +65,37 @@ class BlocklistConnector extends Component {
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchBlocklist();
|
||||
}
|
||||
};
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoBlocklistPreviousPage();
|
||||
}
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoBlocklistNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoBlocklistLastPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoBlocklistPage({ page });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelected = (ids) => {
|
||||
this.props.removeBlocklistItems({ ids });
|
||||
}
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setBlocklistSort({ sortKey });
|
||||
}
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setBlocklistTableOption(payload);
|
||||
@@ -103,11 +103,11 @@ class BlocklistConnector extends Component {
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onClearBlocklistPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -32,11 +32,11 @@ class BlocklistRow extends Component {
|
||||
|
||||
onDetailsPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -25,6 +25,7 @@ function HistoryDetails(props) {
|
||||
releaseGroup,
|
||||
nzbInfoUrl,
|
||||
downloadClient,
|
||||
downloadClientName,
|
||||
downloadId,
|
||||
age,
|
||||
ageHours,
|
||||
@@ -32,6 +33,8 @@ function HistoryDetails(props) {
|
||||
publishedDate
|
||||
} = data;
|
||||
|
||||
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
@@ -71,11 +74,12 @@ function HistoryDetails(props) {
|
||||
}
|
||||
|
||||
{
|
||||
!!downloadClient &&
|
||||
downloadClientNameInfo ?
|
||||
<DescriptionListItem
|
||||
title={translate('DownloadClient')}
|
||||
data={downloadClient}
|
||||
/>
|
||||
data={downloadClientNameInfo}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -57,38 +57,38 @@ class HistoryConnector extends Component {
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchHistory();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoHistoryFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoHistoryPreviousPage();
|
||||
}
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoHistoryNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoHistoryLastPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoHistoryPage({ page });
|
||||
}
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setHistorySort({ sortKey });
|
||||
}
|
||||
};
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setHistoryFilter({ selectedFilterKey });
|
||||
}
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setHistoryTableOption(payload);
|
||||
@@ -96,7 +96,7 @@ class HistoryConnector extends Component {
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoHistoryFirstPage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -41,11 +41,11 @@ class HistoryRow extends Component {
|
||||
|
||||
onDetailsPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -179,6 +179,16 @@ class HistoryRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sourceTitle') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'details') {
|
||||
return (
|
||||
<TableRowCell
|
||||
|
||||
@@ -46,7 +46,7 @@ class HistoryRowConnector extends Component {
|
||||
|
||||
onMarkAsFailedPress = () => {
|
||||
this.props.markAsFailed({ id: this.props.id });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -90,45 +90,45 @@ class Queue extends Component {
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQueueRowModalOpenOrClose = (isOpen) => {
|
||||
this._shouldBlockRefresh = isOpen;
|
||||
}
|
||||
};
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onGrabSelectedPress = () => {
|
||||
this.props.onGrabSelectedPress(this.getSelectedIds());
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true }, () => {
|
||||
this._shouldBlockRefresh = true;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedConfirmed = (payload) => {
|
||||
this._shouldBlockRefresh = false;
|
||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this._shouldBlockRefresh = false;
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -77,34 +77,34 @@ class QueueConnector extends Component {
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchQueue();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoQueueFirstPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoQueuePreviousPage();
|
||||
}
|
||||
};
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoQueueNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoQueueLastPage();
|
||||
}
|
||||
};
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoQueuePage({ page });
|
||||
}
|
||||
};
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setQueueSort({ sortKey });
|
||||
}
|
||||
};
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setQueueTableOption(payload);
|
||||
@@ -112,21 +112,21 @@ class QueueConnector extends Component {
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoQueueFirstPage();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onGrabSelectedPress = (ids) => {
|
||||
this.props.grabQueueItems({ ids });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedPress = (payload) => {
|
||||
this.props.removeQueueItems(payload);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -42,7 +42,7 @@ class QueueOptions extends Component {
|
||||
[name]: value
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -40,7 +40,7 @@ class QueueRow extends Component {
|
||||
|
||||
onRemoveQueueItemPress = () => {
|
||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveQueueItemModalConfirmed = (blocklist) => {
|
||||
const {
|
||||
@@ -52,25 +52,25 @@ class QueueRow extends Component {
|
||||
onRemoveQueueItemPress(blocklist);
|
||||
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveQueueItemModalClose = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(false);
|
||||
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(true);
|
||||
|
||||
this.setState({ isInteractiveImportModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onInteractiveImportModalClose = () => {
|
||||
this.props.onQueueRowModalOpenOrClose(false);
|
||||
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -37,11 +37,11 @@ class QueueRowConnector extends Component {
|
||||
|
||||
onGrabPress = () => {
|
||||
this.props.grabQueueItem({ id: this.props.id });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveQueueItemPress = (payload) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -34,30 +34,30 @@ class RemoveQueueItemModal extends Component {
|
||||
remove: true,
|
||||
blocklist: false
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
}
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -30,35 +30,35 @@ class RemoveQueueItemsModal extends Component {
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false
|
||||
});
|
||||
}
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
}
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -67,12 +67,12 @@ class AddNewMovie extends Component {
|
||||
this.props.onClearMovieLookup();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onClearMovieLookupPress = () => {
|
||||
this.setState({ term: '' });
|
||||
this.props.onClearMovieLookup();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -161,7 +161,7 @@ class AddNewMovie extends Component {
|
||||
{translate('YouCanAlsoSearch')}
|
||||
</div>
|
||||
<div>
|
||||
<Link to="https://wiki.servarr.com/radarr/faq#why-cant-i-add-a-new-movie-to-radarr">
|
||||
<Link to="https://wiki.servarr.com/radarr/faq#why-can-i-not-add-a-new-movie-to-radarr">
|
||||
{translate('CantFindMovie')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -79,11 +79,11 @@ class AddNewMovieConnector extends Component {
|
||||
this.props.lookupMovie({ term });
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onClearMovieLookup = () => {
|
||||
this.props.clearAddMovie();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -22,11 +22,11 @@ class AddNewMovieModalContent extends Component {
|
||||
|
||||
onQualityProfileIdChange = ({ value }) => {
|
||||
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
||||
}
|
||||
};
|
||||
|
||||
onAddMoviePress = () => {
|
||||
this.props.onAddMoviePress();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -51,7 +51,7 @@ class AddNewMovieModalContentConnector extends Component {
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setAddMovieDefault({ [name]: value });
|
||||
}
|
||||
};
|
||||
|
||||
onAddMoviePress = () => {
|
||||
const {
|
||||
@@ -73,7 +73,7 @@ class AddNewMovieModalContentConnector extends Component {
|
||||
searchForMovie: searchForMovie.value,
|
||||
tags: tags.value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
@@ -39,15 +39,15 @@ class AddNewMovieSearchResult extends Component {
|
||||
|
||||
onPress = () => {
|
||||
this.setState({ isNewAddMovieModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onAddMovieModalClose = () => {
|
||||
this.setState({ isNewAddMovieModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onExternalLinkPress = (event) => {
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -86,6 +86,13 @@ class AddNewMovieSearchResult extends Component {
|
||||
} = this.state;
|
||||
|
||||
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
|
||||
const posterWidth = 167;
|
||||
const posterHeight = 250;
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}px`
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.searchResult}>
|
||||
@@ -102,6 +109,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
<div className={styles.posterContainer}>
|
||||
<MoviePoster
|
||||
className={styles.poster}
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={250}
|
||||
overflow={true}
|
||||
@@ -114,7 +122,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
monitored={monitored}
|
||||
hasFile={hasFile}
|
||||
status={status}
|
||||
posterWidth={167}
|
||||
posterWidth={posterWidth}
|
||||
detailedProgressBar={true}
|
||||
queueStatus={queueStatus}
|
||||
queueState={queueState}
|
||||
@@ -182,8 +190,8 @@ class AddNewMovieSearchResult extends Component {
|
||||
|
||||
<div>
|
||||
<Label size={sizes.LARGE}>
|
||||
<HeartRating
|
||||
rating={ratings.value}
|
||||
<TmdbRating
|
||||
ratings={ratings}
|
||||
iconSize={13}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
@@ -32,25 +32,25 @@ class ImportMovie extends Component {
|
||||
|
||||
setScrollerRef = (ref) => {
|
||||
this.setState({ scroller: ref });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState, { parseIds: false });
|
||||
}
|
||||
};
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
// Only select non-dupes
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveSelectedStateItem = (id) => {
|
||||
this.setState((state) => {
|
||||
@@ -62,15 +62,15 @@ class ImportMovie extends Component {
|
||||
selectedState
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.onInputChange(this.getSelectedIds(), name, value);
|
||||
}
|
||||
};
|
||||
|
||||
onImportPress = () => {
|
||||
this.props.onImportPress(this.getSelectedIds());
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -112,11 +112,11 @@ class ImportMovieConnector extends Component {
|
||||
[name]: value
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onImportPress = (ids) => {
|
||||
this.props.dispatchImportMovie({ ids });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -83,7 +83,7 @@ class ImportMovieFooter extends Component {
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.setState({ [name]: value });
|
||||
this.props.onInputChange({ name, value });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -48,7 +48,7 @@ class ImportMovieRowConnector extends Component {
|
||||
id: this.props.id,
|
||||
[name]: value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -121,7 +121,7 @@ class ImportMovieTable extends Component {
|
||||
/>
|
||||
</VirtualTableRow>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
.movie {
|
||||
.container {
|
||||
display: flex;
|
||||
padding: 10px 20px;
|
||||
width: 100%;
|
||||
|
||||
@@ -6,3 +7,19 @@
|
||||
background-color: $menuItemHoverBackgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
.movie {
|
||||
flex: 1 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tmdbLink {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
margin-left: auto;
|
||||
color: $textColor;
|
||||
}
|
||||
|
||||
.tmdbLinkIcon {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import ImportMovieTitle from './ImportMovieTitle';
|
||||
import styles from './ImportMovieSearchResult.css';
|
||||
|
||||
@@ -11,13 +13,14 @@ class ImportMovieSearchResult extends Component {
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.tmdbId);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
tmdbId,
|
||||
title,
|
||||
year,
|
||||
studio,
|
||||
@@ -25,17 +28,30 @@ class ImportMovieSearchResult extends Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.movie}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<ImportMovieTitle
|
||||
title={title}
|
||||
year={year}
|
||||
network={studio}
|
||||
isExistingMovie={isExistingMovie}
|
||||
/>
|
||||
</Link>
|
||||
<div className={styles.container}>
|
||||
<Link
|
||||
className={styles.movie}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<ImportMovieTitle
|
||||
title={title}
|
||||
year={year}
|
||||
network={studio}
|
||||
isExistingMovie={isExistingMovie}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className={styles.tmdbLink}
|
||||
to={`https://www.themoviedb.org/movie/${tmdbId}`}
|
||||
>
|
||||
<Icon
|
||||
className={styles.tmdbLinkIcon}
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={16}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class ImportMovieSelectMovie extends Component {
|
||||
this.setState({ isOpen: false });
|
||||
this._removeListener();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onPress = () => {
|
||||
if (this.state.isOpen) {
|
||||
@@ -79,7 +79,7 @@ class ImportMovieSelectMovie extends Component {
|
||||
}
|
||||
|
||||
this.setState({ isOpen: !this.state.isOpen });
|
||||
}
|
||||
};
|
||||
|
||||
onSearchInputChange = ({ value }) => {
|
||||
if (this._movieLookupTimeout) {
|
||||
@@ -91,17 +91,17 @@ class ImportMovieSelectMovie extends Component {
|
||||
this.props.onSearchInputChange(value);
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.onSearchInputChange(this.state.term);
|
||||
}
|
||||
};
|
||||
|
||||
onMovieSelect = (tmdbId) => {
|
||||
this.setState({ isOpen: false });
|
||||
|
||||
this.props.onMovieSelect(tmdbId);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -36,7 +36,7 @@ class ImportMovieSelectMovieConnector extends Component {
|
||||
term,
|
||||
topOfQueue: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onMovieSelect = (tmdbId) => {
|
||||
const {
|
||||
@@ -48,7 +48,7 @@ class ImportMovieSelectMovieConnector extends Component {
|
||||
id,
|
||||
selectedMovie: _.find(items, { tmdbId })
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -25,7 +25,7 @@ class ImportMovieRootFolderRowConnector extends Component {
|
||||
|
||||
onDeletePress = () => {
|
||||
this.props.deleteRootFolder({ id: this.props.id });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -55,15 +55,15 @@ class ImportMovieSelectFolder extends Component {
|
||||
|
||||
onAddNewRootFolderPress = () => {
|
||||
this.setState({ isAddNewRootFolderModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onNewRootFolderSelect = ({ value }) => {
|
||||
this.props.onNewRootFolderSelect(value);
|
||||
}
|
||||
};
|
||||
|
||||
onAddRootFolderModalClose = () => {
|
||||
this.setState({ isAddNewRootFolderModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -58,11 +58,11 @@ class ImportMovieSelectFolderConnector extends Component {
|
||||
|
||||
onNewRootFolderSelect = (path) => {
|
||||
this.props.addRootFolder({ path });
|
||||
}
|
||||
};
|
||||
|
||||
onDeleteRootFolderPress = (id) => {
|
||||
this.props.deleteRootFolder({ id });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
|
||||
|
||||
function App({ store, history }) {
|
||||
return (
|
||||
<DocumentTitle title="Radarr">
|
||||
<DocumentTitle title={window.Radarr.instanceName}>
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<PageConnector>
|
||||
|
||||
@@ -7,6 +7,7 @@ import QueueConnector from 'Activity/Queue/QueueConnector';
|
||||
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
||||
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||
import CollectionConnector from 'Collection/CollectionConnector';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
|
||||
@@ -72,6 +73,11 @@ function AppRoutes(props) {
|
||||
component={AddNewMovieConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/collections"
|
||||
component={CollectionConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/add/import"
|
||||
component={ImportMovies}
|
||||
|
||||
@@ -27,11 +27,11 @@ class AgendaEvent extends Component {
|
||||
|
||||
onPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -119,43 +119,43 @@ class CalendarConnector extends Component {
|
||||
|
||||
this.props.fetchQueueDetails({ time, view });
|
||||
this.props.fetchCalendar({ time, view });
|
||||
}
|
||||
};
|
||||
|
||||
scheduleUpdate = () => {
|
||||
this.clearUpdateTimeout();
|
||||
|
||||
this.updateTimeoutId = setTimeout(this.updateCalendar, UPDATE_DELAY);
|
||||
}
|
||||
};
|
||||
|
||||
clearUpdateTimeout = () => {
|
||||
if (this.updateTimeoutId) {
|
||||
clearTimeout(this.updateTimeoutId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateCalendar = () => {
|
||||
this.props.gotoCalendarToday();
|
||||
this.scheduleUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onCalendarViewChange = (view) => {
|
||||
this.props.setCalendarView({ view });
|
||||
}
|
||||
};
|
||||
|
||||
onTodayPress = () => {
|
||||
this.props.gotoCalendarToday();
|
||||
}
|
||||
};
|
||||
|
||||
onPreviousPress = () => {
|
||||
this.props.gotoCalendarPreviousRange();
|
||||
}
|
||||
};
|
||||
|
||||
onNextPress = () => {
|
||||
this.props.gotoCalendarNextRange();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -44,23 +44,23 @@ class CalendarPage extends Component {
|
||||
const days = Math.max(3, Math.min(7, Math.floor(width / MINIMUM_DAY_WIDTH)));
|
||||
|
||||
this.props.onDaysCountChange(days);
|
||||
}
|
||||
};
|
||||
|
||||
onGetCalendarLinkPress = () => {
|
||||
this.setState({ isCalendarLinkModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onGetCalendarLinkModalClose = () => {
|
||||
this.setState({ isCalendarLinkModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onOptionsPress = () => {
|
||||
this.setState({ isOptionsModalOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
onOptionsModalClose = () => {
|
||||
this.setState({ isOptionsModalOpen: false });
|
||||
}
|
||||
};
|
||||
|
||||
onSearchMissingPress = () => {
|
||||
const {
|
||||
@@ -69,7 +69,7 @@ class CalendarPage extends Component {
|
||||
} = this.props;
|
||||
|
||||
onSearchMissingPress(missingMovieIds);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -60,20 +60,20 @@ class CalendarDays extends Component {
|
||||
this.setState({ todaysDate: todaysDate.toISOString() });
|
||||
|
||||
this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff);
|
||||
}
|
||||
};
|
||||
|
||||
clearUpdateTimeout = () => {
|
||||
if (this.updateTimeoutId) {
|
||||
clearTimeout(this.updateTimeoutId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEventModalOpenToggle = (isEventModalOpen) => {
|
||||
this.setState({ isEventModalOpen });
|
||||
}
|
||||
};
|
||||
|
||||
onTouchStart = (event) => {
|
||||
const touches = event.touches;
|
||||
@@ -92,7 +92,7 @@ class CalendarDays extends Component {
|
||||
}
|
||||
|
||||
this._touchStart = touchStart;
|
||||
}
|
||||
};
|
||||
|
||||
onTouchEnd = (event) => {
|
||||
const touches = event.changedTouches;
|
||||
@@ -109,17 +109,17 @@ class CalendarDays extends Component {
|
||||
}
|
||||
|
||||
this._touchStart = null;
|
||||
}
|
||||
};
|
||||
|
||||
onTouchCancel = (event) => {
|
||||
this._touchStart = null;
|
||||
}
|
||||
};
|
||||
|
||||
onTouchMove = (event) => {
|
||||
if (!this._touchStart) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -47,13 +47,13 @@ class DaysOfWeek extends Component {
|
||||
});
|
||||
|
||||
this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff);
|
||||
}
|
||||
};
|
||||
|
||||
clearUpdateTimeout = () => {
|
||||
if (this.updateTimeoutId) {
|
||||
clearTimeout(this.updateTimeoutId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -43,15 +43,15 @@ class CalendarEvent extends Component {
|
||||
const link = `/movie/${titleSlug}`;
|
||||
const eventType = [];
|
||||
|
||||
if (moment(date).isSame(moment(inCinemas), 'day')) {
|
||||
if (inCinemas && moment(date).isSame(moment(inCinemas), 'day')) {
|
||||
eventType.push('Cinemas');
|
||||
}
|
||||
|
||||
if (moment(date).isSame(moment(physicalRelease), 'day')) {
|
||||
if (physicalRelease && moment(date).isSame(moment(physicalRelease), 'day')) {
|
||||
eventType.push('Physical');
|
||||
}
|
||||
|
||||
if (moment(date).isSame(moment(digitalRelease), 'day')) {
|
||||
if (digitalRelease && moment(date).isSame(moment(digitalRelease), 'day')) {
|
||||
eventType.push('Digital');
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ class CalendarHeader extends Component {
|
||||
this.setState({ view }, () => {
|
||||
this.props.onViewChange(view);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -41,19 +41,19 @@ class CalendarHeaderConnector extends Component {
|
||||
|
||||
onViewChange = (view) => {
|
||||
this.props.setCalendarView({ view });
|
||||
}
|
||||
};
|
||||
|
||||
onTodayPress = () => {
|
||||
this.props.gotoCalendarToday();
|
||||
}
|
||||
};
|
||||
|
||||
onPreviousPress = () => {
|
||||
this.props.gotoCalendarPreviousRange();
|
||||
}
|
||||
};
|
||||
|
||||
onNextPress = () => {
|
||||
this.props.gotoCalendarNextRange();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -12,7 +12,7 @@ class CalendarHeaderViewButton extends Component {
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.view);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -69,7 +69,7 @@ class CalendarOptionsModalContent extends Component {
|
||||
} = this.props;
|
||||
|
||||
dispatchSetCalendarOption({ [name]: value });
|
||||
}
|
||||
};
|
||||
|
||||
onGlobalInputChange = ({ name, value }) => {
|
||||
const {
|
||||
@@ -81,11 +81,11 @@ class CalendarOptionsModalContent extends Component {
|
||||
this.setState(setting, () => {
|
||||
dispatchSaveUISettings(setting);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onLinkFocus = (event) => {
|
||||
event.target.select();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -84,11 +84,11 @@ class CalendarLinkModalContent extends Component {
|
||||
[name]: value,
|
||||
...urls
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onLinkFocus = (event) => {
|
||||
event.target.select();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
31
frontend/src/Collection/AddNewCollectionMovieModal.js
Normal file
31
frontend/src/Collection/AddNewCollectionMovieModal.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AddNewCollectionMovieModalContentConnector from './AddNewCollectionMovieModalContentConnector';
|
||||
|
||||
function AddNewCollectionMovieModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AddNewCollectionMovieModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddNewCollectionMovieModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewCollectionMovieModal;
|
||||
@@ -0,0 +1,68 @@
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.year {
|
||||
margin-left: 5px;
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.poster {
|
||||
flex: 0 0 170px;
|
||||
margin-right: 20px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.overview {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.labelIcon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.searchForMissingMovieLabelContainer {
|
||||
display: flex;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.searchForMissingMovieLabel {
|
||||
margin-right: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.searchForMissingMovieContainer {
|
||||
composes: container from '~Components/Form/CheckInput.css';
|
||||
|
||||
flex: 0 1 0;
|
||||
}
|
||||
|
||||
.searchForMissingMovieInput {
|
||||
composes: input from '~Components/Form/CheckInput.css';
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||
}
|
||||
|
||||
.addButton {
|
||||
@add-mixin truncate;
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.modalFooter {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
204
frontend/src/Collection/AddNewCollectionMovieModalContent.js
Normal file
204
frontend/src/Collection/AddNewCollectionMovieModalContent.js
Normal file
@@ -0,0 +1,204 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AddNewCollectionMovieModalContent.css';
|
||||
|
||||
class AddNewCollectionMovieModalContent extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQualityProfileIdChange = ({ value }) => {
|
||||
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
||||
};
|
||||
|
||||
onAddMoviePress = () => {
|
||||
this.props.onAddMoviePress();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
year,
|
||||
overview,
|
||||
images,
|
||||
isAdding,
|
||||
folder,
|
||||
tags,
|
||||
isSmallScreen,
|
||||
isWindows,
|
||||
onModalClose,
|
||||
onInputChange,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
minimumAvailability,
|
||||
searchForMovie
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{title}
|
||||
|
||||
{
|
||||
!title.contains(year) && !!year &&
|
||||
<span className={styles.year}>({year})</span>
|
||||
}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.container}>
|
||||
{
|
||||
!isSmallScreen &&
|
||||
<div className={styles.poster}>
|
||||
<MoviePoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.info}>
|
||||
<div className={styles.overview}>
|
||||
{overview}
|
||||
</div>
|
||||
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
name="rootFolderPath"
|
||||
valueOptions={{
|
||||
movieFolder: folder,
|
||||
isWindows
|
||||
}}
|
||||
selectedValueOptions={{
|
||||
movieFolder: folder,
|
||||
isWindows
|
||||
}}
|
||||
helpText={translate('SubfolderWillBeCreatedAutomaticallyInterp', [folder])}
|
||||
onChange={onInputChange}
|
||||
{...rootFolderPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Monitor')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MOVIE_MONITORED_SELECT}
|
||||
name="monitor"
|
||||
onChange={onInputChange}
|
||||
{...monitor}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
name="minimumAvailability"
|
||||
onChange={onInputChange}
|
||||
{...minimumAvailability}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
onChange={this.onQualityProfileIdChange}
|
||||
{...qualityProfileId}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
onChange={onInputChange}
|
||||
{...tags}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<label className={styles.searchForMissingMovieLabelContainer}>
|
||||
<span className={styles.searchForMissingMovieLabel}>
|
||||
{translate('StartSearchForMissingMovie')}
|
||||
</span>
|
||||
|
||||
<CheckInput
|
||||
containerClassName={styles.searchForMissingMovieContainer}
|
||||
className={styles.searchForMissingMovieInput}
|
||||
name="searchForMovie"
|
||||
onChange={onInputChange}
|
||||
{...searchForMovie}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.addButton}
|
||||
kind={kinds.SUCCESS}
|
||||
isSpinning={isAdding}
|
||||
onPress={this.onAddMoviePress}
|
||||
>
|
||||
{translate('AddMovie')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewCollectionMovieModalContent.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
overview: PropTypes.string,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
minimumAvailability: PropTypes.object.isRequired,
|
||||
searchForMovie: PropTypes.object.isRequired,
|
||||
folder: PropTypes.string.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
isWindows: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onAddMoviePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewCollectionMovieModalContent;
|
||||
@@ -0,0 +1,121 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { addMovie, setMovieCollectionValue } from 'Store/Actions/movieCollectionActions';
|
||||
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import AddNewMovieModalContent from './AddNewCollectionMovieModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movieCollections,
|
||||
createCollectionSelector(),
|
||||
createDimensionsSelector(),
|
||||
createSystemStatusSelector(),
|
||||
(discoverMovieState, collection, dimensions, systemStatus) => {
|
||||
const {
|
||||
isAdding,
|
||||
addError,
|
||||
pendingChanges
|
||||
} = discoverMovieState;
|
||||
|
||||
const collectionDefaults = {
|
||||
rootFolderPath: collection.rootFolderPath,
|
||||
monitor: 'movieOnly',
|
||||
qualityProfileId: collection.qualityProfileId,
|
||||
minimumAvailability: collection.minimumAvailability,
|
||||
searchForMovie: collection.searchOnAdd,
|
||||
tags: []
|
||||
};
|
||||
|
||||
const {
|
||||
settings,
|
||||
validationErrors,
|
||||
validationWarnings
|
||||
} = selectSettings(collectionDefaults, pendingChanges, addError);
|
||||
|
||||
return {
|
||||
isAdding,
|
||||
addError,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
isWindows: systemStatus.isWindows,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
addMovie,
|
||||
setMovieCollectionValue
|
||||
};
|
||||
|
||||
class AddNewCollectionMovieModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setMovieCollectionValue({ name, value });
|
||||
};
|
||||
|
||||
onAddMoviePress = () => {
|
||||
const {
|
||||
tmdbId,
|
||||
title,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
minimumAvailability,
|
||||
searchForMovie,
|
||||
tags
|
||||
} = this.props;
|
||||
|
||||
this.props.addMovie({
|
||||
tmdbId,
|
||||
title,
|
||||
rootFolderPath: rootFolderPath.value,
|
||||
monitor: monitor.value,
|
||||
qualityProfileId: qualityProfileId.value,
|
||||
minimumAvailability: minimumAvailability.value,
|
||||
searchForMovie: searchForMovie.value,
|
||||
tags: tags.value
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AddNewMovieModalContent
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onAddMoviePress={this.onAddMoviePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewCollectionMovieModalContentConnector.propTypes = {
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
minimumAvailability: PropTypes.object.isRequired,
|
||||
searchForMovie: PropTypes.object.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
addMovie: PropTypes.func.isRequired,
|
||||
setMovieCollectionValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewCollectionMovieModalContentConnector);
|
||||
403
frontend/src/Collection/Collection.js
Normal file
403
frontend/src/Collection/Collection.js
Normal file
@@ -0,0 +1,403 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageJumpBar from 'Components/Page/PageJumpBar';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import styles from 'Movie/Index/MovieIndex.css';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import CollectionFooter from './CollectionFooter';
|
||||
import CollectionFilterMenu from './Menus/CollectionFilterMenu';
|
||||
import CollectionSortMenu from './Menus/CollectionSortMenu';
|
||||
import NoCollection from './NoCollection';
|
||||
import CollectionOverviewsConnector from './Overview/CollectionOverviewsConnector';
|
||||
import CollectionOverviewOptionsModal from './Overview/Options/CollectionOverviewOptionsModal';
|
||||
|
||||
function getViewComponent(view) {
|
||||
return CollectionOverviewsConnector;
|
||||
}
|
||||
|
||||
class Collection extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
scroller: null,
|
||||
jumpBarItems: { order: [] },
|
||||
jumpToCharacter: null,
|
||||
isPosterOptionsModalOpen: false,
|
||||
isOverviewOptionsModalOpen: false,
|
||||
isConfirmSearchModalOpen: false,
|
||||
searchType: null,
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setJumpBarItems();
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection
|
||||
} = this.props;
|
||||
|
||||
if (sortKey !== prevProps.sortKey ||
|
||||
sortDirection !== prevProps.sortDirection ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||
) {
|
||||
this.setJumpBarItems();
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
if (this.state.jumpToCharacter != null) {
|
||||
this.setState({ jumpToCharacter: null });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setScrollerRef = (ref) => {
|
||||
this.setState({ scroller: ref });
|
||||
};
|
||||
|
||||
getSelectedIds = () => {
|
||||
if (this.state.allUnselected) {
|
||||
return [];
|
||||
}
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
};
|
||||
|
||||
setSelectedState() {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const newSelectedState = {};
|
||||
|
||||
items.forEach((collection) => {
|
||||
const isItemSelected = selectedState[collection.id];
|
||||
|
||||
if (isItemSelected) {
|
||||
newSelectedState[collection.id] = isItemSelected;
|
||||
} else {
|
||||
newSelectedState[collection.id] = false;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedCount = getSelectedIds(newSelectedState).length;
|
||||
const newStateCount = Object.keys(newSelectedState).length;
|
||||
let isAllSelected = false;
|
||||
let isAllUnselected = false;
|
||||
|
||||
if (selectedCount === 0) {
|
||||
isAllUnselected = true;
|
||||
} else if (selectedCount === newStateCount) {
|
||||
isAllSelected = true;
|
||||
}
|
||||
|
||||
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
||||
}
|
||||
|
||||
setJumpBarItems() {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection
|
||||
} = this.props;
|
||||
|
||||
// Reset if not sorting by sortTitle
|
||||
if (sortKey !== 'sortTitle') {
|
||||
this.setState({ jumpBarItems: { order: [] } });
|
||||
return;
|
||||
}
|
||||
|
||||
const characters = _.reduce(items, (acc, item) => {
|
||||
let char = item.sortTitle.charAt(0);
|
||||
|
||||
if (!isNaN(char)) {
|
||||
char = '#';
|
||||
}
|
||||
|
||||
if (char in acc) {
|
||||
acc[char] = acc[char] + 1;
|
||||
} else {
|
||||
acc[char] = 1;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const order = Object.keys(characters).sort();
|
||||
|
||||
// Reverse if sorting descending
|
||||
if (sortDirection === sortDirections.DESCENDING) {
|
||||
order.reverse();
|
||||
}
|
||||
|
||||
const jumpBarItems = {
|
||||
characters,
|
||||
order
|
||||
};
|
||||
|
||||
this.setState({ jumpBarItems });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onOverviewOptionsPress = () => {
|
||||
this.setState({ isOverviewOptionsModalOpen: true });
|
||||
};
|
||||
|
||||
onOverviewOptionsModalClose = () => {
|
||||
this.setState({ isOverviewOptionsModalOpen: false });
|
||||
};
|
||||
|
||||
onJumpBarItemPress = (jumpToCharacter) => {
|
||||
this.setState({ jumpToCharacter });
|
||||
};
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
};
|
||||
|
||||
onSelectAllPress = () => {
|
||||
this.onSelectAllChange({ value: !this.state.allSelected });
|
||||
};
|
||||
|
||||
onRefreshMovieCollectionsPress = () => {
|
||||
this.props.onRefreshMovieCollectionsPress();
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey, 'id');
|
||||
});
|
||||
};
|
||||
|
||||
onUpdateSelectedPress = (changes) => {
|
||||
this.props.onUpdateSelectedPress({
|
||||
collectionIds: this.getSelectedIds(),
|
||||
...changes
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
totalItems,
|
||||
items,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
view,
|
||||
onSortSelect,
|
||||
onFilterSelect,
|
||||
onScroll,
|
||||
isRefreshingCollections,
|
||||
isSaving,
|
||||
isAdding,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
scroller,
|
||||
jumpBarItems,
|
||||
jumpToCharacter,
|
||||
isOverviewOptionsModalOpen,
|
||||
selectedState,
|
||||
allSelected,
|
||||
allUnselected
|
||||
} = this.state;
|
||||
|
||||
const selectedMovieIds = this.getSelectedIds();
|
||||
|
||||
const ViewComponent = getViewComponent(view);
|
||||
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
||||
const hasNoCollection = !totalItems;
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('RefreshCollections')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isRefreshingCollections}
|
||||
isDisabled={hasNoCollection}
|
||||
onPress={this.onRefreshMovieCollectionsPress}
|
||||
/>
|
||||
<PageToolbarButton
|
||||
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
|
||||
iconName={icons.CHECK_SQUARE}
|
||||
isDisabled={hasNoCollection}
|
||||
onPress={this.onSelectAllPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection
|
||||
alignContent={align.RIGHT}
|
||||
collapseButtons={false}
|
||||
>
|
||||
{
|
||||
view === 'overview' ?
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.OVERVIEW}
|
||||
onPress={this.onOverviewOptionsPress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
(view === 'posters' || view === 'overview') &&
|
||||
<PageToolbarSeparator />
|
||||
}
|
||||
|
||||
<CollectionSortMenu
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
isDisabled={hasNoCollection}
|
||||
onSortSelect={onSortSelect}
|
||||
/>
|
||||
|
||||
<CollectionFilterMenu
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
isDisabled={hasNoCollection}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<div className={styles.pageContentBodyWrapper}>
|
||||
<PageContentBody
|
||||
registerScroller={this.setScrollerRef}
|
||||
className={styles.contentBody}
|
||||
innerClassName={styles[`${view}InnerContentBody`]}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
{translate('UnableToLoadCollections')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isLoaded &&
|
||||
<div className={styles.contentBodyContainer}>
|
||||
<ViewComponent
|
||||
scroller={scroller}
|
||||
items={items}
|
||||
filters={filters}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
jumpToCharacter={jumpToCharacter}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
selectedState={selectedState}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoCollection totalItems={totalItems} />
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
isLoaded && !!jumpBarItems.order.length &&
|
||||
<PageJumpBar
|
||||
items={jumpBarItems}
|
||||
onItemPress={this.onJumpBarItemPress}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
isLoaded &&
|
||||
<CollectionFooter
|
||||
selectedIds={selectedMovieIds}
|
||||
isSaving={isSaving}
|
||||
isAdding={isAdding}
|
||||
onUpdateSelectedPress={this.onUpdateSelectedPress}
|
||||
/>
|
||||
}
|
||||
|
||||
<CollectionOverviewOptionsModal
|
||||
isOpen={isOverviewOptionsModalOpen}
|
||||
onModalClose={this.onOverviewOptionsModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Collection.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
totalItems: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
view: PropTypes.string.isRequired,
|
||||
isRefreshingCollections: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onSortSelect: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired,
|
||||
onUpdateSelectedPress: PropTypes.func.isRequired,
|
||||
onRefreshMovieCollectionsPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Collection;
|
||||
108
frontend/src/Collection/CollectionConnector.js
Normal file
108
frontend/src/Collection/CollectionConnector.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withScrollPosition from 'Components/withScrollPosition';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { saveMovieCollections, setMovieCollectionsFilter, setMovieCollectionsSort } from 'Store/Actions/movieCollectionActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Collection from './Collection';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createCollectionClientSideCollectionItemsSelector('movieCollections'),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_COLLECTIONS),
|
||||
createDimensionsSelector(),
|
||||
(
|
||||
collections,
|
||||
isRefreshingCollections,
|
||||
dimensionsState
|
||||
) => {
|
||||
return {
|
||||
...collections,
|
||||
isRefreshingCollections,
|
||||
isSmallScreen: dimensionsState.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchFetchRootFolders() {
|
||||
dispatch(fetchRootFolders());
|
||||
},
|
||||
onUpdateSelectedPress(payload) {
|
||||
dispatch(saveMovieCollections(payload));
|
||||
},
|
||||
onSortSelect(sortKey) {
|
||||
dispatch(setMovieCollectionsSort({ sortKey }));
|
||||
},
|
||||
onFilterSelect(selectedFilterKey) {
|
||||
dispatch(setMovieCollectionsFilter({ selectedFilterKey }));
|
||||
},
|
||||
onRefreshMovieCollectionsPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.REFRESH_COLLECTIONS
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class CollectionConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
registerPagePopulator(this.repopulate);
|
||||
this.props.dispatchFetchRootFolders();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onScroll = ({ scrollTop }) => {
|
||||
scrollPositions.movieCollections = scrollTop;
|
||||
};
|
||||
|
||||
onUpdateSelectedPress = (payload) => {
|
||||
this.props.onUpdateSelectedPress(payload);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Collection
|
||||
{...this.props}
|
||||
onViewSelect={this.onViewSelect}
|
||||
onScroll={this.onScroll}
|
||||
onUpdateSelectedPress={this.onUpdateSelectedPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionConnector.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
view: PropTypes.string.isRequired,
|
||||
onUpdateSelectedPress: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withScrollPosition(
|
||||
connect(createMapStateToProps, createMapDispatchToProps)(CollectionConnector),
|
||||
'movieCollections'
|
||||
);
|
||||
24
frontend/src/Collection/CollectionFilterModalConnector.js
Normal file
24
frontend/src/Collection/CollectionFilterModalConnector.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setMovieCollectionsFilter } from 'Store/Actions/movieCollectionActions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movieCollections.items,
|
||||
(state) => state.movieCollections.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'movieCollections'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetFilter: setMovieCollectionsFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
||||
56
frontend/src/Collection/CollectionFooter.css
Normal file
56
frontend/src/Collection/CollectionFooter.css
Normal file
@@ -0,0 +1,56 @@
|
||||
.inputContainer {
|
||||
margin-right: 20px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.buttonContainerContent {
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.addSelectedButton {
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
|
||||
margin-right: 10px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.excludeSelectedButton {
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
|
||||
margin-left: 25px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.inputContainer {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.buttonContainerContent {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.selectedMovieLabel {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
236
frontend/src/Collection/CollectionFooter.js
Normal file
236
frontend/src/Collection/CollectionFooter.js
Normal file
@@ -0,0 +1,236 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AvailabilitySelectInput from 'Components/Form/AvailabilitySelectInput';
|
||||
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
|
||||
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CollectionFooterLabel from './CollectionFooterLabel';
|
||||
import styles from './CollectionFooter.css';
|
||||
|
||||
const NO_CHANGE = 'noChange';
|
||||
|
||||
class CollectionFooter extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
monitor: NO_CHANGE,
|
||||
monitored: NO_CHANGE,
|
||||
qualityProfileId: NO_CHANGE,
|
||||
minimumAvailability: NO_CHANGE,
|
||||
rootFolderPath: NO_CHANGE,
|
||||
destinationRootFolder: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isSaving,
|
||||
saveError
|
||||
} = this.props;
|
||||
|
||||
const newState = {};
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
this.setState({
|
||||
monitored: NO_CHANGE,
|
||||
monitor: NO_CHANGE,
|
||||
qualityProfileId: NO_CHANGE,
|
||||
rootFolderPath: NO_CHANGE,
|
||||
minimumAvailability: NO_CHANGE
|
||||
});
|
||||
}
|
||||
|
||||
if (!_.isEmpty(newState)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.setState({ [name]: value });
|
||||
};
|
||||
|
||||
onUpdateSelectedPress = () => {
|
||||
const {
|
||||
monitor,
|
||||
monitored,
|
||||
qualityProfileId,
|
||||
minimumAvailability,
|
||||
rootFolderPath
|
||||
} = this.state;
|
||||
|
||||
const changes = {};
|
||||
|
||||
if (monitored !== NO_CHANGE) {
|
||||
changes.monitored = monitored === 'monitored';
|
||||
}
|
||||
|
||||
if (monitor !== NO_CHANGE) {
|
||||
changes.monitor = monitor;
|
||||
}
|
||||
|
||||
if (qualityProfileId !== NO_CHANGE) {
|
||||
changes.qualityProfileId = qualityProfileId;
|
||||
}
|
||||
|
||||
if (minimumAvailability !== NO_CHANGE) {
|
||||
changes.minimumAvailability = minimumAvailability;
|
||||
}
|
||||
|
||||
if (rootFolderPath !== NO_CHANGE) {
|
||||
changes.rootFolderPath = rootFolderPath;
|
||||
}
|
||||
|
||||
this.props.onUpdateSelectedPress(changes);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
selectedIds,
|
||||
isSaving
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitored,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
minimumAvailability,
|
||||
rootFolderPath
|
||||
} = this.state;
|
||||
|
||||
const monitoredOptions = [
|
||||
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
||||
{ key: 'monitored', value: translate('Monitored') },
|
||||
{ key: 'unmonitored', value: translate('Unmonitored') }
|
||||
];
|
||||
|
||||
const selectedCount = selectedIds.length;
|
||||
|
||||
return (
|
||||
<PageContentFooter>
|
||||
<div className={styles.inputContainer}>
|
||||
<CollectionFooterLabel
|
||||
label={translate('MonitorCollection')}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
name="monitored"
|
||||
value={monitored}
|
||||
values={monitoredOptions}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<CollectionFooterLabel
|
||||
label={translate('MonitorMovies')}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
name="monitor"
|
||||
value={monitor}
|
||||
values={monitoredOptions}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<CollectionFooterLabel
|
||||
label={translate('QualityProfile')}
|
||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<QualityProfileSelectInputConnector
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<CollectionFooterLabel
|
||||
label={translate('MinimumAvailability')}
|
||||
isSaving={isSaving && minimumAvailability !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<AvailabilitySelectInput
|
||||
name="minimumAvailability"
|
||||
value={minimumAvailability}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<CollectionFooterLabel
|
||||
label={translate('RootFolder')}
|
||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<RootFolderSelectInputConnector
|
||||
name="rootFolderPath"
|
||||
value={rootFolderPath}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
selectedValueOptions={{ includeFreeSpace: false }}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.buttonContainerContent}>
|
||||
<CollectionFooterLabel
|
||||
label={translate('CollectionsSelectedInterp', [selectedCount])}
|
||||
isSaving={false}
|
||||
/>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<div>
|
||||
<SpinnerButton
|
||||
className={styles.addSelectedButton}
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!selectedCount || isSaving}
|
||||
onPress={this.onUpdateSelectedPress}
|
||||
>
|
||||
{translate('UpdateSelected')}
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PageContentFooter>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionFooter.propTypes = {
|
||||
selectedIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
onUpdateSelectedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CollectionFooter;
|
||||
8
frontend/src/Collection/CollectionFooterLabel.css
Normal file
8
frontend/src/Collection/CollectionFooterLabel.css
Normal file
@@ -0,0 +1,8 @@
|
||||
.label {
|
||||
margin-bottom: 3px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.savingIcon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
40
frontend/src/Collection/CollectionFooterLabel.js
Normal file
40
frontend/src/Collection/CollectionFooterLabel.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import SpinnerIcon from 'Components/SpinnerIcon';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import styles from './CollectionFooterLabel.css';
|
||||
|
||||
function CollectionFooterLabel(props) {
|
||||
const {
|
||||
className,
|
||||
label,
|
||||
isSaving
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{label}
|
||||
|
||||
{
|
||||
isSaving &&
|
||||
<SpinnerIcon
|
||||
className={styles.savingIcon}
|
||||
name={icons.SPINNER}
|
||||
isSpinning={true}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CollectionFooterLabel.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
CollectionFooterLabel.defaultProps = {
|
||||
className: styles.label
|
||||
};
|
||||
|
||||
export default CollectionFooterLabel;
|
||||
75
frontend/src/Collection/CollectionItemConnector.js
Normal file
75
frontend/src/Collection/CollectionItemConnector.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createCollectionSelector(),
|
||||
createAllMoviesSelector(),
|
||||
(
|
||||
collection,
|
||||
allMovies
|
||||
) => {
|
||||
// If a movie is deleted this selector may fire before the parent
|
||||
// selecors, which will result in an undefined movie, if that happens
|
||||
// we want to return early here and again in the render function to avoid
|
||||
// trying to show a movie that has no information available.
|
||||
|
||||
if (!collection) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let allGenres = [];
|
||||
let libraryMovies = 0;
|
||||
|
||||
collection.movies.forEach((movie) => {
|
||||
allGenres = allGenres.concat(movie.genres);
|
||||
|
||||
if (allMovies.find((libraryMovie) => libraryMovie.tmdbId === movie.tmdbId)) {
|
||||
libraryMovies++;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
...collection,
|
||||
genres: Array.from(new Set(allGenres)).slice(0, 3),
|
||||
missingMovies: collection.movies.length - libraryMovies
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class CollectionItemConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
component: ItemComponent,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemComponent
|
||||
{...otherProps}
|
||||
id={id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionItemConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
component: PropTypes.elementType.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(CollectionItemConnector);
|
||||
25
frontend/src/Collection/Edit/EditCollectionModal.js
Normal file
25
frontend/src/Collection/Edit/EditCollectionModal.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import EditCollectionModalContentConnector from './EditCollectionModalContentConnector';
|
||||
|
||||
function EditCollectionModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<EditCollectionModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
EditCollectionModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditCollectionModal;
|
||||
39
frontend/src/Collection/Edit/EditCollectionModalConnector.js
Normal file
39
frontend/src/Collection/Edit/EditCollectionModalConnector.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import EditCollectionModal from './EditCollectionModal';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class EditCollectionModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'movieCollections' });
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditCollectionModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditCollectionModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(EditCollectionModalConnector);
|
||||
17
frontend/src/Collection/Edit/EditCollectionModalContent.css
Normal file
17
frontend/src/Collection/Edit/EditCollectionModalContent.css
Normal file
@@ -0,0 +1,17 @@
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.poster {
|
||||
flex: 0 0 170px;
|
||||
margin-right: 20px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.overview {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
178
frontend/src/Collection/Edit/EditCollectionModalContent.js
Normal file
178
frontend/src/Collection/Edit/EditCollectionModalContent.js
Normal file
@@ -0,0 +1,178 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './EditCollectionModalContent.css';
|
||||
|
||||
class EditCollectionModalContent extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSavePress = () => {
|
||||
const {
|
||||
onSavePress
|
||||
} = this.props;
|
||||
|
||||
onSavePress(false);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
images,
|
||||
overview,
|
||||
item,
|
||||
isSaving,
|
||||
onInputChange,
|
||||
onModalClose,
|
||||
isSmallScreen,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitored,
|
||||
qualityProfileId,
|
||||
minimumAvailability,
|
||||
// Id,
|
||||
rootFolderPath,
|
||||
searchOnAdd
|
||||
} = item;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('Edit')} - {title}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.container}>
|
||||
{
|
||||
!isSmallScreen &&
|
||||
<div className={styles.poster}>
|
||||
<MoviePoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.info}>
|
||||
<div className={styles.overview}>
|
||||
{overview}
|
||||
</div>
|
||||
|
||||
<Form
|
||||
{...otherProps}
|
||||
>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Monitored')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="monitored"
|
||||
helpText={translate('MonitoredCollectionHelpText')}
|
||||
{...monitored}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
name="minimumAvailability"
|
||||
{...minimumAvailability}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
{...qualityProfileId}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Folder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
name="rootFolderPath"
|
||||
{...rootFolderPath}
|
||||
includeMissingValue={true}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="searchOnAdd"
|
||||
helpText={translate('SearchOnAddCollectionHelpText')}
|
||||
{...searchOnAdd}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<SpinnerButton
|
||||
isSpinning={isSaving}
|
||||
onPress={this.onSavePress}
|
||||
>
|
||||
{translate('Save')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditCollectionModalContent.propTypes = {
|
||||
collectionId: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
item: PropTypes.object.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isPathChanging: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default EditCollectionModalContent;
|
||||
@@ -0,0 +1,119 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveMovieCollection, setMovieCollectionValue } from 'Store/Actions/movieCollectionActions';
|
||||
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import EditCollectionModalContent from './EditCollectionModalContent';
|
||||
|
||||
function createIsPathChangingSelector() {
|
||||
return createSelector(
|
||||
(state) => state.movieCollections.pendingChanges,
|
||||
createCollectionSelector(),
|
||||
(pendingChanges, collection) => {
|
||||
const rootFolderPath = pendingChanges.rootFolderPath;
|
||||
|
||||
if (rootFolderPath == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return collection.rootFolderPath !== rootFolderPath;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movieCollections,
|
||||
createCollectionSelector(),
|
||||
createIsPathChangingSelector(),
|
||||
createDimensionsSelector(),
|
||||
(moviesState, collection, isPathChanging, dimensions) => {
|
||||
const {
|
||||
isSaving,
|
||||
saveError,
|
||||
pendingChanges
|
||||
} = moviesState;
|
||||
|
||||
const movieSettings = {
|
||||
monitored: collection.monitored,
|
||||
qualityProfileId: collection.qualityProfileId,
|
||||
minimumAvailability: collection.minimumAvailability,
|
||||
rootFolderPath: collection.rootFolderPath,
|
||||
searchOnAdd: collection.searchOnAdd
|
||||
};
|
||||
|
||||
const settings = selectSettings(movieSettings, pendingChanges, saveError);
|
||||
|
||||
return {
|
||||
title: collection.title,
|
||||
images: collection.images,
|
||||
overview: collection.overview,
|
||||
isSaving,
|
||||
saveError,
|
||||
isPathChanging,
|
||||
originalPath: collection.path,
|
||||
item: settings.settings,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetMovieCollectionValue: setMovieCollectionValue,
|
||||
dispatchSaveMovieCollection: saveMovieCollection
|
||||
};
|
||||
|
||||
class EditCollectionModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetMovieCollectionValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.dispatchSaveMovieCollection({
|
||||
id: this.props.collectionId
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EditCollectionModalContent
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
onMoveMoviePress={this.onMoveMoviePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditCollectionModalContentConnector.propTypes = {
|
||||
collectionId: PropTypes.number,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
dispatchSetMovieCollectionValue: PropTypes.func.isRequired,
|
||||
dispatchSaveMovieCollection: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditCollectionModalContentConnector);
|
||||
41
frontend/src/Collection/Menus/CollectionFilterMenu.js
Normal file
41
frontend/src/Collection/Menus/CollectionFilterMenu.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import CollectionFilterModalConnector from 'Collection/CollectionFilterModalConnector';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import { align } from 'Helpers/Props';
|
||||
|
||||
function CollectionFilterMenu(props) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
isDisabled,
|
||||
onFilterSelect
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
isDisabled={isDisabled}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={CollectionFilterModalConnector}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
CollectionFilterMenu.propTypes = {
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
CollectionFilterMenu.defaultProps = {
|
||||
showCustomFilters: false
|
||||
};
|
||||
|
||||
export default CollectionFilterMenu;
|
||||
43
frontend/src/Collection/Menus/CollectionSortMenu.js
Normal file
43
frontend/src/Collection/Menus/CollectionSortMenu.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import SortMenu from 'Components/Menu/SortMenu';
|
||||
import SortMenuItem from 'Components/Menu/SortMenuItem';
|
||||
import { align, sortDirections } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function CollectionSortMenu(props) {
|
||||
const {
|
||||
sortKey,
|
||||
sortDirection,
|
||||
isDisabled,
|
||||
onSortSelect
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<SortMenu
|
||||
isDisabled={isDisabled}
|
||||
alignMenu={align.RIGHT}
|
||||
>
|
||||
<MenuContent>
|
||||
<SortMenuItem
|
||||
name="sortTitle"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
{translate('Title')}
|
||||
</SortMenuItem>
|
||||
</MenuContent>
|
||||
</SortMenu>
|
||||
);
|
||||
}
|
||||
|
||||
CollectionSortMenu.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onSortSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CollectionSortMenu;
|
||||
11
frontend/src/Collection/NoCollection.css
Normal file
11
frontend/src/Collection/NoCollection.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.message {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
52
frontend/src/Collection/NoCollection.js
Normal file
52
frontend/src/Collection/NoCollection.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './NoCollection.css';
|
||||
|
||||
function NoCollection(props) {
|
||||
const { totalItems } = props;
|
||||
|
||||
if (totalItems > 0) {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.message}>
|
||||
{translate('AllCollectionsHiddenDueToFilter')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.message}>
|
||||
{translate('NoCollections')}
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
to="/add/import"
|
||||
kind={kinds.PRIMARY}
|
||||
>
|
||||
{translate('ImportExistingMovies')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<Button
|
||||
to="/add/new"
|
||||
kind={kinds.PRIMARY}
|
||||
>
|
||||
{translate('AddNewMovie')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NoCollection.propTypes = {
|
||||
totalItems: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default NoCollection;
|
||||
117
frontend/src/Collection/Overview/CollectionMovie.css
Normal file
117
frontend/src/Collection/Overview/CollectionMovie.css
Normal file
@@ -0,0 +1,117 @@
|
||||
$hoverScale: 1.05;
|
||||
|
||||
.content {
|
||||
border-radius: 5px;
|
||||
transition: all 200ms ease-in;
|
||||
|
||||
&:hover {
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 10px $black;
|
||||
transition: all 200ms ease-in;
|
||||
|
||||
.poster {
|
||||
opacity: 0.5;
|
||||
transition: opacity 100ms linear 100ms;
|
||||
}
|
||||
|
||||
.overlayTitle {
|
||||
opacity: 1;
|
||||
transition: opacity 100ms linear 100ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.posterContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.poster {
|
||||
position: relative;
|
||||
display: block;
|
||||
background-color: $defaultColor;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.overlayTitle {
|
||||
padding: 5px;
|
||||
color: $offWhite;
|
||||
text-align: left;
|
||||
font-weight: bold;
|
||||
font-size: 15px;
|
||||
opacity: 0;
|
||||
transition: opacity 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
@add-mixin truncate;
|
||||
|
||||
background-color: #fafbfc;
|
||||
text-align: center;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
z-index: 3;
|
||||
border-radius: 4px;
|
||||
background-color: #707070;
|
||||
color: $white;
|
||||
font-size: $smallFontSize;
|
||||
opacity: 0;
|
||||
transition: opacity 0;
|
||||
}
|
||||
|
||||
.action {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
&:hover {
|
||||
color: $radarrYellow;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.container {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.editorSelect {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.externalLinks {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
.link {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
position: relative;
|
||||
display: block;
|
||||
background-color: $defaultColor;
|
||||
}
|
||||
|
||||
.monitorToggleButton {
|
||||
composes: toggleButton from '~Components/MonitorToggleButton.css';
|
||||
|
||||
width: 25px;
|
||||
color: $white;
|
||||
|
||||
&:hover {
|
||||
color: $iconButtonHoverLightColor;
|
||||
}
|
||||
}
|
||||
191
frontend/src/Collection/Overview/CollectionMovie.js
Normal file
191
frontend/src/Collection/Overview/CollectionMovie.js
Normal file
@@ -0,0 +1,191 @@
|
||||
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 MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import AddNewCollectionMovieModal from './../AddNewCollectionMovieModal';
|
||||
import styles from './CollectionMovie.css';
|
||||
|
||||
class CollectionMovie extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
hasPosterError: false,
|
||||
isEditMovieModalOpen: false,
|
||||
isNewAddMovieModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditMoviePress = () => {
|
||||
this.setState({ isEditMovieModalOpen: true });
|
||||
};
|
||||
|
||||
onEditMovieModalClose = () => {
|
||||
this.setState({ isEditMovieModalOpen: false });
|
||||
};
|
||||
|
||||
onAddMoviePress = () => {
|
||||
this.setState({ isNewAddMovieModalOpen: true });
|
||||
};
|
||||
|
||||
onAddMovieModalClose = () => {
|
||||
this.setState({ isNewAddMovieModalOpen: false });
|
||||
};
|
||||
|
||||
onPosterLoad = () => {
|
||||
if (this.state.hasPosterError) {
|
||||
this.setState({ hasPosterError: false });
|
||||
}
|
||||
};
|
||||
|
||||
onPosterLoadError = () => {
|
||||
if (!this.state.hasPosterError) {
|
||||
this.setState({ hasPosterError: true });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
overview,
|
||||
year,
|
||||
tmdbId,
|
||||
images,
|
||||
monitored,
|
||||
hasFile,
|
||||
folder,
|
||||
isAvailable,
|
||||
isExistingMovie,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
detailedProgressBar,
|
||||
onMonitorTogglePress,
|
||||
collectionId
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isEditMovieModalOpen,
|
||||
isNewAddMovieModalOpen
|
||||
} = this.state;
|
||||
|
||||
const linkProps = id ? { to: `/movie/${tmdbId}` } : { onPress: this.onAddMoviePress };
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}px`,
|
||||
borderRadius: '5px'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.posterContainer}>
|
||||
{
|
||||
isExistingMovie &&
|
||||
<div className={styles.editorSelect}>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
size={20}
|
||||
onPress={onMonitorTogglePress}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
style={elementStyle}
|
||||
{...linkProps}
|
||||
>
|
||||
<MoviePoster
|
||||
className={styles.poster}
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
onError={this.onPosterLoadError}
|
||||
onLoad={this.onPosterLoad}
|
||||
/>
|
||||
|
||||
<div className={styles.overlay}>
|
||||
<div className={styles.overlayTitle}>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
{
|
||||
id &&
|
||||
<div className={styles.overlayStatus}>
|
||||
<MovieIndexProgressBar
|
||||
monitored={monitored}
|
||||
hasFile={hasFile}
|
||||
status={status}
|
||||
bottomRadius={true}
|
||||
posterWidth={posterWidth}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
isAvailable={isAvailable}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<AddNewCollectionMovieModal
|
||||
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
|
||||
tmdbId={tmdbId}
|
||||
title={title}
|
||||
year={year}
|
||||
overview={overview}
|
||||
images={images}
|
||||
folder={folder}
|
||||
onModalClose={this.onAddMovieModalClose}
|
||||
collectionId={collectionId}
|
||||
/>
|
||||
|
||||
<EditMovieModalConnector
|
||||
isOpen={isEditMovieModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onEditMovieModalClose}
|
||||
onDeleteMoviePress={this.onDeleteMoviePress}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionMovie.propTypes = {
|
||||
id: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
overview: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool,
|
||||
collectionId: PropTypes.number.isRequired,
|
||||
hasFile: PropTypes.bool,
|
||||
folder: PropTypes.string,
|
||||
isAvailable: PropTypes.bool,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
posterWidth: PropTypes.number.isRequired,
|
||||
posterHeight: PropTypes.number.isRequired,
|
||||
detailedProgressBar: PropTypes.bool.isRequired,
|
||||
isExistingMovie: PropTypes.bool,
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
imdbId: PropTypes.string,
|
||||
youTubeTrailerId: PropTypes.string,
|
||||
onMonitorTogglePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CollectionMovie;
|
||||
59
frontend/src/Collection/Overview/CollectionMovieConnector.js
Normal file
59
frontend/src/Collection/Overview/CollectionMovieConnector.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||
import createCollectionExistingMovieSelector from 'Store/Selectors/createCollectionExistingMovieSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import CollectionMovie from './CollectionMovie';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createDimensionsSelector(),
|
||||
createCollectionExistingMovieSelector(),
|
||||
(dimensions, existingMovie) => {
|
||||
return {
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
isExistingMovie: !!existingMovie,
|
||||
...existingMovie
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleMovieMonitored
|
||||
};
|
||||
|
||||
class CollectionMovieConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMonitorTogglePress = (monitored) => {
|
||||
this.props.toggleMovieMonitored({
|
||||
movieId: this.props.id,
|
||||
monitored
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CollectionMovie
|
||||
{...this.props}
|
||||
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionMovieConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
monitored: PropTypes.bool,
|
||||
toggleMovieMonitored: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(CollectionMovieConnector);
|
||||
50
frontend/src/Collection/Overview/CollectionMovieLabel.css
Normal file
50
frontend/src/Collection/Overview/CollectionMovieLabel.css
Normal file
@@ -0,0 +1,50 @@
|
||||
.movie {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
overflow: hidden;
|
||||
margin: 2px 4px;
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: #eee;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.movieTitle {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.movieStatus {
|
||||
padding: 0 4px;
|
||||
border-left: 4px;
|
||||
border-left-style: solid;
|
||||
background-color: $white;
|
||||
color: $defaultColor;
|
||||
}
|
||||
|
||||
.primary {
|
||||
border-color: $primaryColor;
|
||||
}
|
||||
|
||||
.danger {
|
||||
border-color: $dangerColor;
|
||||
}
|
||||
|
||||
.success {
|
||||
border-color: $successColor;
|
||||
}
|
||||
|
||||
.purple {
|
||||
border-color: $purple;
|
||||
}
|
||||
|
||||
.warning {
|
||||
border-color: $warningColor;
|
||||
}
|
||||
|
||||
.info {
|
||||
border-color: $infoColor;
|
||||
}
|
||||
|
||||
.queue {
|
||||
border-color: $queueColor;
|
||||
}
|
||||
84
frontend/src/Collection/Overview/CollectionMovieLabel.js
Normal file
84
frontend/src/Collection/Overview/CollectionMovieLabel.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './CollectionMovieLabel.css';
|
||||
|
||||
class CollectionMovieLabel extends Component {
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
status,
|
||||
monitored,
|
||||
isAvailable,
|
||||
hasFile,
|
||||
onMonitorTogglePress,
|
||||
isSaving
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.movie}>
|
||||
<div className={styles.movieTitle}>
|
||||
{
|
||||
id &&
|
||||
<MonitorToggleButton
|
||||
monitored={monitored}
|
||||
isSaving={isSaving}
|
||||
onPress={onMonitorTogglePress}
|
||||
/>
|
||||
}
|
||||
|
||||
<span>
|
||||
{
|
||||
title
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{
|
||||
id &&
|
||||
<div
|
||||
className={classNames(
|
||||
styles.movieStatus,
|
||||
styles[getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds')]
|
||||
)}
|
||||
>
|
||||
{
|
||||
hasFile ? translate('Downloaded') : translate('Missing')
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionMovieLabel.propTypes = {
|
||||
id: PropTypes.number,
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string,
|
||||
isAvailable: PropTypes.bool,
|
||||
monitored: PropTypes.bool,
|
||||
hasFile: PropTypes.bool,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
movieFile: PropTypes.object,
|
||||
movieFileId: PropTypes.number,
|
||||
onMonitorTogglePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
CollectionMovieLabel.defaultProps = {
|
||||
isSaving: false,
|
||||
statistics: {
|
||||
episodeFileCount: 0,
|
||||
totalEpisodeCount: 0,
|
||||
percentOfEpisodes: 0
|
||||
}
|
||||
};
|
||||
|
||||
export default CollectionMovieLabel;
|
||||
@@ -0,0 +1,59 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||
import createCollectionExistingMovieSelector from 'Store/Selectors/createCollectionExistingMovieSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import CollectionMovieLabel from './CollectionMovieLabel';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createDimensionsSelector(),
|
||||
createCollectionExistingMovieSelector(),
|
||||
(dimensions, existingMovie) => {
|
||||
return {
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
isExistingMovie: !!existingMovie,
|
||||
...existingMovie
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleMovieMonitored
|
||||
};
|
||||
|
||||
class CollectionMovieLabelConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMonitorTogglePress = (monitored) => {
|
||||
this.props.toggleMovieMonitored({
|
||||
movieId: this.props.id,
|
||||
monitored
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CollectionMovieLabel
|
||||
{...this.props}
|
||||
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionMovieLabelConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
monitored: PropTypes.bool,
|
||||
toggleMovieMonitored: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(CollectionMovieLabelConnector);
|
||||
137
frontend/src/Collection/Overview/CollectionOverview.css
Normal file
137
frontend/src/Collection/Overview/CollectionOverview.css
Normal file
@@ -0,0 +1,137 @@
|
||||
$hoverScale: 1.05;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.editorSelect {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 5px;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.toggleMonitoredContainer {
|
||||
align-self: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.sliderContainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.labelsContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.moviesContainer {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.movie {
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex: 1 0 1px;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
@add-mixin truncate;
|
||||
|
||||
font-weight: 300;
|
||||
font-size: 30px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 1 0 auto;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.defaults {
|
||||
margin-bottom: 5px;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.detailsLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
margin: 5px 10px 5px 0;
|
||||
}
|
||||
|
||||
.path,
|
||||
.status,
|
||||
.genres,
|
||||
.qualityProfileName {
|
||||
margin-left: 8px;
|
||||
font-weight: 300;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.overview {
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.monitorToggleButton {
|
||||
composes: toggleButton from '~Components/MonitorToggleButton.css';
|
||||
|
||||
width: 25px;
|
||||
|
||||
&:hover {
|
||||
color: $iconButtonHoverLightColor;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.navigationButtons {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.title {
|
||||
@add-mixin truncate;
|
||||
|
||||
font-weight: 300;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.toggleMonitoredContainer {
|
||||
align-self: center;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.monitorToggleButton {
|
||||
composes: toggleButton from '~Components/MonitorToggleButton.css';
|
||||
|
||||
width: 20px;
|
||||
|
||||
&:hover {
|
||||
color: $iconButtonHoverLightColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
340
frontend/src/Collection/Overview/CollectionOverview.js
Normal file
340
frontend/src/Collection/Overview/CollectionOverview.js
Normal file
@@ -0,0 +1,340 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Slider from 'react-slick';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import EditCollectionModalConnector from 'Collection/Edit/EditCollectionModalConnector';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Icon from 'Components/Icon';
|
||||
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 dimensions from 'Styles/Variables/dimensions';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import CollectionMovieConnector from './CollectionMovieConnector';
|
||||
import CollectionMovieLabelConnector from './CollectionMovieLabelConnector';
|
||||
import styles from './CollectionOverview.css';
|
||||
|
||||
import 'slick-carousel/slick/slick.css';
|
||||
import 'slick-carousel/slick/slick-theme.css';
|
||||
|
||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||
const lineHeight = parseFloat(fonts.lineHeight);
|
||||
|
||||
// Hardcoded height beased on line-height of 32 + bottom margin of 10. 19 + 5 for List Row
|
||||
// Less side-effecty than using react-measure.
|
||||
const titleRowHeight = 100;
|
||||
|
||||
function getContentHeight(rowHeight, isSmallScreen) {
|
||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||
|
||||
return rowHeight - padding;
|
||||
}
|
||||
|
||||
class CollectionOverview extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditCollectionModalOpen: false,
|
||||
isNewAddMovieModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setSliderRef = (ref) => {
|
||||
this.setState({ slider: ref });
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
this.setState({ isNewAddMovieModalOpen: true });
|
||||
};
|
||||
|
||||
onEditCollectionPress = () => {
|
||||
this.setState({ isEditCollectionModalOpen: true });
|
||||
};
|
||||
|
||||
onEditCollectionModalClose = () => {
|
||||
this.setState({ isEditCollectionModalOpen: false });
|
||||
};
|
||||
|
||||
onAddMovieModalClose = () => {
|
||||
this.setState({ isNewAddMovieModalOpen: false });
|
||||
};
|
||||
|
||||
onChange = ({ value, shiftKey }) => {
|
||||
const {
|
||||
id,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value, shiftKey });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
monitored,
|
||||
qualityProfileId,
|
||||
rootFolderPath,
|
||||
genres,
|
||||
id,
|
||||
title,
|
||||
movies,
|
||||
overview,
|
||||
missingMovies,
|
||||
posterHeight,
|
||||
posterWidth,
|
||||
rowHeight,
|
||||
isSmallScreen,
|
||||
isSelected,
|
||||
onMonitorTogglePress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
showDetails,
|
||||
showOverview,
|
||||
showPosters,
|
||||
detailedProgressBar
|
||||
} = this.props.overviewOptions;
|
||||
|
||||
const {
|
||||
isEditCollectionModalOpen
|
||||
} = this.state;
|
||||
|
||||
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
||||
const overviewHeight = contentHeight - titleRowHeight - posterHeight;
|
||||
|
||||
const sliderSettings = {
|
||||
arrows: false,
|
||||
dots: false,
|
||||
infinite: false,
|
||||
slidesToShow: 1,
|
||||
slidesToScroll: 1,
|
||||
variableWidth: true
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.editorSelect}>
|
||||
<CheckInput
|
||||
className={styles.checkInput}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.info} style={{ maxHeight: contentHeight }}>
|
||||
|
||||
<div className={styles.titleRow}>
|
||||
<div className={styles.titleContainer}>
|
||||
<div className={styles.toggleMonitoredContainer}>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
size={isSmallScreen ? 20 : 25}
|
||||
onPress={onMonitorTogglePress}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
name={icons.EDIT}
|
||||
title={translate('EditCollection')}
|
||||
onPress={this.onEditCollectionPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
showPosters &&
|
||||
<div className={styles.navigationButtons}>
|
||||
<IconButton
|
||||
name={icons.ARROW_LEFT}
|
||||
title={translate('ScrollMovies')}
|
||||
onPress={this.state.slider?.slickPrev}
|
||||
size={20}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={icons.ARROW_RIGHT}
|
||||
title={translate('ScrollMovies')}
|
||||
onPress={this.state.slider?.slickNext}
|
||||
size={20}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
{
|
||||
showDetails &&
|
||||
<div className={styles.defaults}>
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<Icon
|
||||
name={icons.DRIVE}
|
||||
size={13}
|
||||
/>
|
||||
<span className={styles.status}>
|
||||
{`${missingMovies} missing movie(s)`}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
{
|
||||
!isSmallScreen &&
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<Icon
|
||||
name={icons.PROFILE}
|
||||
size={13}
|
||||
/>
|
||||
<span className={styles.qualityProfileName}>
|
||||
{
|
||||
<QualityProfileNameConnector
|
||||
qualityProfileId={qualityProfileId}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
!isSmallScreen &&
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<Icon
|
||||
name={icons.FOLDER}
|
||||
size={13}
|
||||
/>
|
||||
<span className={styles.path}>
|
||||
{rootFolderPath}
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
!isSmallScreen &&
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<Icon
|
||||
name={icons.PROFILE}
|
||||
size={13}
|
||||
/>
|
||||
<span className={styles.genres}>
|
||||
{genres.join(', ')}
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showOverview &&
|
||||
<div className={styles.details}>
|
||||
<div className={styles.overview}>
|
||||
<TextTruncate
|
||||
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
||||
text={overview}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showPosters ?
|
||||
<div className={styles.sliderContainer}>
|
||||
<Slider ref={this.setSliderRef} {...sliderSettings}>
|
||||
{movies.map((movie) => (
|
||||
<div className={styles.movie} key={movie.tmdbId}>
|
||||
<CollectionMovieConnector
|
||||
key={movie.tmdbId}
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
collectionId={id}
|
||||
{...movie}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Slider>
|
||||
</div> :
|
||||
<div className={styles.labelsContainer}>
|
||||
{movies.map((movie) => (
|
||||
<CollectionMovieLabelConnector
|
||||
key={movie.tmdbId}
|
||||
collectionId={id}
|
||||
{...movie}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditCollectionModalConnector
|
||||
isOpen={isEditCollectionModalOpen}
|
||||
collectionId={id}
|
||||
onModalClose={this.onEditCollectionModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionOverview.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
minimumAvailability: PropTypes.string.isRequired,
|
||||
searchOnAdd: PropTypes.bool.isRequired,
|
||||
rootFolderPath: PropTypes.string.isRequired,
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string.isRequired,
|
||||
movies: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
missingMovies: PropTypes.number.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
rowHeight: PropTypes.number.isRequired,
|
||||
posterHeight: PropTypes.number.isRequired,
|
||||
posterWidth: PropTypes.number.isRequired,
|
||||
overviewOptions: PropTypes.object.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onMonitorTogglePress: PropTypes.func.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CollectionOverview;
|
||||
@@ -0,0 +1,55 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { toggleCollectionMonitored } from 'Store/Actions/movieCollectionActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import CollectionOverview from './CollectionOverview';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createDimensionsSelector(),
|
||||
(dimensions) => {
|
||||
return {
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
toggleCollectionMonitored
|
||||
};
|
||||
|
||||
class CollectionOverviewConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMonitorTogglePress = (monitored) => {
|
||||
this.props.toggleCollectionMonitored({
|
||||
collectionId: this.props.collectionId,
|
||||
monitored
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CollectionOverview
|
||||
{...this.props}
|
||||
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionOverviewConnector.propTypes = {
|
||||
collectionId: PropTypes.number.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
toggleCollectionMonitored: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(CollectionOverviewConnector);
|
||||
15
frontend/src/Collection/Overview/CollectionOverviews.css
Normal file
15
frontend/src/Collection/Overview/CollectionOverviews.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.grid {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
&:hover {
|
||||
.content {
|
||||
background-color: $tableRowHoverBackgroundColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.externalLinks {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
272
frontend/src/Collection/Overview/CollectionOverviews.js
Normal file
272
frontend/src/Collection/Overview/CollectionOverviews.js
Normal file
@@ -0,0 +1,272 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Grid, WindowScroller } from 'react-virtualized';
|
||||
import CollectionItemConnector from 'Collection/CollectionItemConnector';
|
||||
import Measure from 'Components/Measure';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import CollectionOverviewConnector from './CollectionOverviewConnector';
|
||||
import styles from './CollectionOverviews.css';
|
||||
|
||||
// Poster container dimensions
|
||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||
|
||||
function calculatePosterWidth(posterSize, isSmallScreen) {
|
||||
const maxiumPosterWidth = isSmallScreen ? 152 : 162;
|
||||
|
||||
if (posterSize === 'large') {
|
||||
return maxiumPosterWidth;
|
||||
}
|
||||
|
||||
if (posterSize === 'medium') {
|
||||
return Math.floor(maxiumPosterWidth * 0.75);
|
||||
}
|
||||
|
||||
return Math.floor(maxiumPosterWidth * 0.5);
|
||||
}
|
||||
|
||||
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
|
||||
|
||||
const heights = [
|
||||
overviewOptions.showPosters ? posterHeight : 75,
|
||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||
];
|
||||
|
||||
return heights.reduce((acc, height) => acc + height + 80, 0);
|
||||
}
|
||||
|
||||
function calculatePosterHeight(posterWidth) {
|
||||
return Math.ceil((250 / 170) * posterWidth);
|
||||
}
|
||||
|
||||
class CollectionOverviews extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
width: 0,
|
||||
columnCount: 1,
|
||||
posterWidth: 162,
|
||||
posterHeight: 238,
|
||||
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {})
|
||||
};
|
||||
|
||||
this._grid = null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
overviewOptions,
|
||||
jumpToCharacter,
|
||||
scrollTop,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
rowHeight,
|
||||
scrollRestored
|
||||
} = this.state;
|
||||
|
||||
if (prevProps.sortKey !== sortKey ||
|
||||
prevProps.overviewOptions !== overviewOptions) {
|
||||
this.calculateGrid(this.state.width, isSmallScreen);
|
||||
}
|
||||
|
||||
if (
|
||||
this._grid &&
|
||||
(prevState.width !== width ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items) ||
|
||||
prevProps.overviewOptions !== overviewOptions)) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
||||
this.setState({ scrollRestored: true });
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
|
||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (this._grid && index != null) {
|
||||
|
||||
this._grid.scrollToCell({
|
||||
rowIndex: index,
|
||||
columnIndex: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setGridRef = (ref) => {
|
||||
this._grid = ref;
|
||||
};
|
||||
|
||||
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||
const {
|
||||
sortKey,
|
||||
overviewOptions
|
||||
} = this.props;
|
||||
|
||||
const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen);
|
||||
const posterHeight = calculatePosterHeight(posterWidth);
|
||||
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions);
|
||||
|
||||
this.setState({
|
||||
width,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
rowHeight
|
||||
});
|
||||
};
|
||||
|
||||
cellRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
overviewOptions,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
isSmallScreen,
|
||||
selectedState,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
rowHeight
|
||||
} = this.state;
|
||||
|
||||
const collection = items[rowIndex];
|
||||
|
||||
if (!collection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.container}
|
||||
key={key}
|
||||
style={style}
|
||||
>
|
||||
<CollectionItemConnector
|
||||
key={collection.id}
|
||||
component={CollectionOverviewConnector}
|
||||
sortKey={sortKey}
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
rowHeight={rowHeight}
|
||||
overviewOptions={overviewOptions}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
longDateFormat={longDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
isSmallScreen={isSmallScreen}
|
||||
collectionId={collection.id}
|
||||
isSelected={selectedState[collection.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMeasure = ({ width }) => {
|
||||
this.calculateGrid(width, this.props.isSmallScreen);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isSmallScreen,
|
||||
scroller,
|
||||
items,
|
||||
selectedState
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
rowHeight
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Measure
|
||||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<WindowScroller
|
||||
scrollElement={isSmallScreen ? undefined : scroller}
|
||||
>
|
||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||
if (!height) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={registerChild}>
|
||||
<Grid
|
||||
ref={this.setGridRef}
|
||||
className={styles.grid}
|
||||
autoHeight={true}
|
||||
height={height}
|
||||
columnCount={1}
|
||||
columnWidth={width}
|
||||
rowCount={items.length}
|
||||
rowHeight={rowHeight}
|
||||
width={width}
|
||||
onScroll={onChildScroll}
|
||||
scrollTop={scrollTop}
|
||||
overscanRowCount={2}
|
||||
cellRenderer={this.cellRenderer}
|
||||
selectedState={selectedState}
|
||||
scrollToAlignment={'start'}
|
||||
isScrollingOptout={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</WindowScroller>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionOverviews.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
overviewOptions: PropTypes.object.isRequired,
|
||||
jumpToCharacter: PropTypes.string,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CollectionOverviews;
|
||||
@@ -0,0 +1,25 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import CollectionOverviews from './CollectionOverviews';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movieCollections.overviewOptions,
|
||||
createUISettingsSelector(),
|
||||
createDimensionsSelector(),
|
||||
(overviewOptions, uiSettings, dimensions) => {
|
||||
return {
|
||||
overviewOptions,
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(CollectionOverviews);
|
||||
@@ -0,0 +1,25 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import CollectionOverviewOptionsModalContentConnector from './CollectionOverviewOptionsModalContentConnector';
|
||||
|
||||
function CollectionOverviewOptionsModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<CollectionOverviewOptionsModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
CollectionOverviewOptionsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CollectionOverviewOptionsModal;
|
||||
@@ -0,0 +1,205 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const posterSizeOptions = [
|
||||
{ key: 'small', value: translate('Small') },
|
||||
{ key: 'medium', value: translate('Medium') },
|
||||
{ key: 'large', value: translate('Large') }
|
||||
];
|
||||
|
||||
class CollectionOverviewOptionsModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
detailedProgressBar: props.detailedProgressBar,
|
||||
size: props.size,
|
||||
showDetails: props.showDetails,
|
||||
showOverview: props.showOverview,
|
||||
showPosters: props.showPosters
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
detailedProgressBar,
|
||||
size,
|
||||
showDetails,
|
||||
showOverview,
|
||||
showPosters
|
||||
} = this.props;
|
||||
|
||||
const state = {};
|
||||
|
||||
if (detailedProgressBar !== prevProps.detailedProgressBar) {
|
||||
state.detailedProgressBar = detailedProgressBar;
|
||||
}
|
||||
|
||||
if (size !== prevProps.size) {
|
||||
state.size = size;
|
||||
}
|
||||
|
||||
if (showDetails !== prevProps.showDetails) {
|
||||
state.showDetails = showDetails;
|
||||
}
|
||||
|
||||
if (showOverview !== prevProps.showOverview) {
|
||||
state.showOverview = showOverview;
|
||||
}
|
||||
|
||||
if (showPosters !== prevProps.showPosters) {
|
||||
state.showPosters = showPosters;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(state)) {
|
||||
this.setState(state);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChangeOverviewOption = ({ name, value }) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
}, () => {
|
||||
this.props.onChangeOverviewOption({ [name]: value });
|
||||
});
|
||||
};
|
||||
|
||||
onChangeOption = ({ name, value }) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
}, () => {
|
||||
this.props.onChangeOption({
|
||||
[name]: value
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
size,
|
||||
detailedProgressBar,
|
||||
showDetails,
|
||||
showPosters,
|
||||
showOverview
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('CollectionOptions')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PosterSize')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="size"
|
||||
value={size}
|
||||
values={posterSizeOptions}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('DetailedProgressBar')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="detailedProgressBar"
|
||||
value={detailedProgressBar}
|
||||
helpText={translate('DetailedProgressBarHelpText')}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowCollectionDetails')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showDetails"
|
||||
value={showDetails}
|
||||
helpText={translate('CollectionShowDetailsHelpText')}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowOverview')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showOverview"
|
||||
value={showOverview}
|
||||
helpText={translate('CollectionShowOverviewsHelpText')}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowPosters')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showPosters"
|
||||
value={showPosters}
|
||||
helpText={translate('CollectionShowPostersHelpText')}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CollectionOverviewOptionsModalContent.propTypes = {
|
||||
detailedProgressBar: PropTypes.bool.isRequired,
|
||||
size: PropTypes.string.isRequired,
|
||||
showDetails: PropTypes.bool.isRequired,
|
||||
showOverview: PropTypes.bool.isRequired,
|
||||
showPosters: PropTypes.bool.isRequired,
|
||||
onChangeOverviewOption: PropTypes.func.isRequired,
|
||||
onChangeOption: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CollectionOverviewOptionsModalContent;
|
||||
@@ -0,0 +1,29 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setMovieCollectionsOption, setMovieCollectionsOverviewOption } from 'Store/Actions/movieCollectionActions';
|
||||
import CollectionOverviewOptionsModalContent from './CollectionOverviewOptionsModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movieCollections,
|
||||
(movieCollections) => {
|
||||
return {
|
||||
...movieCollections.options,
|
||||
...movieCollections.overviewOptions
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onChangeOverviewOption(payload) {
|
||||
dispatch(setMovieCollectionsOverviewOption(payload));
|
||||
},
|
||||
onChangeOption(payload) {
|
||||
dispatch(setMovieCollectionsOption(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(CollectionOverviewOptionsModalContent);
|
||||
@@ -10,6 +10,7 @@ export const DOWNLOADED_MOVIES_SCAN = 'DownloadedMoviesScan';
|
||||
export const INTERACTIVE_IMPORT = 'ManualImport';
|
||||
export const MISSING_MOVIES_SEARCH = 'MissingMoviesSearch';
|
||||
export const MOVE_MOVIE = 'MoveMovie';
|
||||
export const REFRESH_COLLECTIONS = 'RefreshCollections';
|
||||
export const REFRESH_MOVIE = 'RefreshMovie';
|
||||
export const RENAME_FILES = 'RenameFiles';
|
||||
export const RENAME_MOVIE = 'RenameMovie';
|
||||
|
||||
@@ -70,18 +70,18 @@ class FileBrowserModalContent extends Component {
|
||||
} else {
|
||||
this._scrollerNode = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPathInputChange = ({ value }) => {
|
||||
this.setState({ currentPath: value });
|
||||
}
|
||||
};
|
||||
|
||||
onRowPress = (path) => {
|
||||
this.props.onFetchPaths(path);
|
||||
}
|
||||
};
|
||||
|
||||
onOkPress = () => {
|
||||
this.props.onChange({
|
||||
@@ -91,7 +91,7 @@ class FileBrowserModalContent extends Component {
|
||||
|
||||
this.props.onClearPaths();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -78,16 +78,16 @@ class FileBrowserModalContentConnector extends Component {
|
||||
allowFoldersWithoutTrailingSlashes: true,
|
||||
includeFiles
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onClearPaths = () => {
|
||||
// this.props.dispatchClearPaths();
|
||||
}
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.dispatchClearPaths();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -28,7 +28,7 @@ class FileBrowserRow extends Component {
|
||||
|
||||
onPress = () => {
|
||||
this.props.onPress(this.props.path);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -103,7 +103,7 @@ class DateFilterBuilderRowValue extends Component {
|
||||
name: NAME,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onTimeChange = ({ value }) => {
|
||||
const {
|
||||
@@ -118,7 +118,7 @@ class DateFilterBuilderRowValue extends Component {
|
||||
value: filterValue.value
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -161,6 +161,7 @@ class DateFilterBuilderRowValue extends Component {
|
||||
<TextInput
|
||||
name={NAME}
|
||||
value={filterValue}
|
||||
type="date"
|
||||
placeholder="yyyy-mm-dd"
|
||||
onChange={this.onValueChange}
|
||||
/>
|
||||
|
||||
@@ -63,7 +63,7 @@ class FilterBuilderModalContent extends Component {
|
||||
|
||||
onLabelChange = ({ value }) => {
|
||||
this.setState({ label: value });
|
||||
}
|
||||
};
|
||||
|
||||
onFilterChange = (index, filter) => {
|
||||
const filters = [...this.state.filters];
|
||||
@@ -72,7 +72,7 @@ class FilterBuilderModalContent extends Component {
|
||||
this.setState({
|
||||
filters
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onAddFilterPress = () => {
|
||||
const filters = [...this.state.filters];
|
||||
@@ -81,7 +81,7 @@ class FilterBuilderModalContent extends Component {
|
||||
this.setState({
|
||||
filters
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onRemoveFilterPress = (index) => {
|
||||
const filters = [...this.state.filters];
|
||||
@@ -90,7 +90,7 @@ class FilterBuilderModalContent extends Component {
|
||||
this.setState({
|
||||
filters
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onSaveFilterPress = () => {
|
||||
const {
|
||||
@@ -122,7 +122,7 @@ class FilterBuilderModalContent extends Component {
|
||||
label,
|
||||
filters
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -166,7 +166,9 @@ class FilterBuilderModalContent extends Component {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.label}>Filters</div>
|
||||
<div className={styles.label}>
|
||||
{translate('Filters')}
|
||||
</div>
|
||||
|
||||
<div className={styles.rows}>
|
||||
{
|
||||
|
||||
@@ -154,7 +154,7 @@ class FilterBuilderRow extends Component {
|
||||
|
||||
this.selectedFilterBuilderProp = selectedFilterBuilderProp;
|
||||
onFilterChange(index, filter);
|
||||
}
|
||||
};
|
||||
|
||||
onFilterChange = ({ name, value }) => {
|
||||
const {
|
||||
@@ -174,7 +174,7 @@ class FilterBuilderRow extends Component {
|
||||
filter[name] = value;
|
||||
|
||||
onFilterChange(index, filter);
|
||||
}
|
||||
};
|
||||
|
||||
onAddPress = () => {
|
||||
const {
|
||||
@@ -183,7 +183,7 @@ class FilterBuilderRow extends Component {
|
||||
} = this.props;
|
||||
|
||||
onAddPress(index);
|
||||
}
|
||||
};
|
||||
|
||||
onRemovePress = () => {
|
||||
const {
|
||||
@@ -192,7 +192,7 @@ class FilterBuilderRow extends Component {
|
||||
} = this.props;
|
||||
|
||||
onRemovePress(index);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -84,7 +84,7 @@ class FilterBuilderRowValue extends Component {
|
||||
name: NAME,
|
||||
value: [...filterValue, value]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onTagDelete = ({ index }) => {
|
||||
const {
|
||||
@@ -98,7 +98,7 @@ class FilterBuilderRowValue extends Component {
|
||||
name: NAME,
|
||||
value
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -47,7 +47,7 @@ class IndexerFilterBuilderRowValueConnector extends Component {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.dispatchFetchIndexers();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -43,7 +43,7 @@ class QualityFilterBuilderRowValueConnector extends Component {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.dispatchFetchQualityProfileSchema();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -55,7 +55,7 @@ class CustomFilter extends Component {
|
||||
} = this.props;
|
||||
|
||||
onEditPress(id);
|
||||
}
|
||||
};
|
||||
|
||||
onRemovePress = () => {
|
||||
const {
|
||||
@@ -67,7 +67,7 @@ class CustomFilter extends Component {
|
||||
dispatchDeleteCustomFilter({ id });
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -25,14 +25,14 @@ class FilterModal extends Component {
|
||||
this.setState({
|
||||
filterBuilder: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onEditCustomFilter = (id) => {
|
||||
this.setState({
|
||||
filterBuilder: true,
|
||||
id
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onCancelPress = () => {
|
||||
if (this.state.filterBuilder) {
|
||||
@@ -43,7 +43,7 @@ class FilterModal extends Component {
|
||||
} else {
|
||||
this.onModalClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({
|
||||
@@ -52,7 +52,7 @@ class FilterModal extends Component {
|
||||
}, () => {
|
||||
this.props.onModalClose();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user