mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-17 21:26:22 -04:00
Compare commits
97 Commits
v5.24.0.10
...
v6-develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c1b3a7b82 | ||
|
|
6396d83fa7 | ||
|
|
bd203d841a | ||
|
|
e96d7580f4 | ||
|
|
eb0f7c62b6 | ||
|
|
41fa0de230 | ||
|
|
cdc6a6dd27 | ||
|
|
1891ac1536 | ||
|
|
2539d46f7c | ||
|
|
32fe345144 | ||
|
|
9d2193636e | ||
|
|
1e898c2647 | ||
|
|
a00ee08750 | ||
|
|
54cbbe05d9 | ||
|
|
57f602eb02 | ||
|
|
e841c9b764 | ||
|
|
81bbaf8946 | ||
|
|
8b4288fa18 | ||
|
|
9aa3061e8e | ||
|
|
308c58f729 | ||
|
|
d38492188a | ||
|
|
50e75e1362 | ||
|
|
f36845c251 | ||
|
|
110a338fb6 | ||
|
|
3fcbaf9259 | ||
|
|
576eff1890 | ||
|
|
b0284bda07 | ||
|
|
c78666009d | ||
|
|
b51d1beaaa | ||
|
|
4d22bf1ceb | ||
|
|
f9562b9b76 | ||
|
|
6851c26328 | ||
|
|
e29be26fc9 | ||
|
|
f6bd2f52d5 | ||
|
|
8bef9b4da7 | ||
|
|
787c387036 | ||
|
|
0525256115 | ||
|
|
5767e181b7 | ||
|
|
1cf3ef5dff | ||
|
|
b6bad2398c | ||
|
|
16308e4b1c | ||
|
|
bd7465fae4 | ||
|
|
c0d70485c3 | ||
|
|
c743383912 | ||
|
|
d93c1d7808 | ||
|
|
0e2e7e4259 | ||
|
|
e6b27512c9 | ||
|
|
dae5e86b2c | ||
|
|
71f032d175 | ||
|
|
5a6db29dbd | ||
|
|
2dac2dd35b | ||
|
|
b829638a77 | ||
|
|
b6b7f13839 | ||
|
|
a9ad197b75 | ||
|
|
1b28116a7e | ||
|
|
5870c88e1c | ||
|
|
0629832bd0 | ||
|
|
430897c710 | ||
|
|
9c42246eef | ||
|
|
489a86b253 | ||
|
|
9c8d3b679d | ||
|
|
b2e51d1613 | ||
|
|
a95b1f2992 | ||
|
|
ac33b15048 | ||
|
|
d28f03af28 | ||
|
|
73b99d0be2 | ||
|
|
15c34a61de | ||
|
|
b99c536306 | ||
|
|
2ebf391f85 | ||
|
|
3945a2eeb8 | ||
|
|
e6980df590 | ||
|
|
187dd79b9c | ||
|
|
22ef334de6 | ||
|
|
c9eb9b8b98 | ||
|
|
9c74c40fc6 | ||
|
|
8911cbe872 | ||
|
|
7e541d4653 | ||
|
|
1cc2237ac0 | ||
|
|
470963921d | ||
|
|
36f9ec4ea7 | ||
|
|
9df2368601 | ||
|
|
e7d76350ec | ||
|
|
fd3828ff5d | ||
|
|
368e1fead8 | ||
|
|
5b357faf16 | ||
|
|
3f35b7c782 | ||
|
|
7d29deb93c | ||
|
|
d0bfdce9c5 | ||
|
|
5d0cd78667 | ||
|
|
afbe0ebcd4 | ||
|
|
bfbb7532a2 | ||
|
|
c92d8c08f1 | ||
|
|
358ce92f85 | ||
|
|
3ec5a4b78a | ||
|
|
cb59ce891a | ||
|
|
4d3d46d796 | ||
|
|
0941e51d27 |
@@ -2,7 +2,7 @@
|
|||||||
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
|
||||||
{
|
{
|
||||||
"name": "Radarr",
|
"name": "Radarr",
|
||||||
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
|
"image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0",
|
||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"nodeGypDependencies": true,
|
"nodeGypDependencies": true,
|
||||||
|
|||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -165,15 +165,12 @@ Thumbs.db
|
|||||||
/tools/Addins/*
|
/tools/Addins/*
|
||||||
packages.config.md5sum
|
packages.config.md5sum
|
||||||
|
|
||||||
|
|
||||||
# Common IntelliJ Platform excludes
|
|
||||||
|
|
||||||
# Ignore Rider projects completely for now
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# ignore node_modules symlink
|
# ignore node_modules symlink
|
||||||
node_modules
|
node_modules
|
||||||
node_modules.nosync
|
node_modules.nosync
|
||||||
|
|
||||||
# API doc generation
|
# API doc generation
|
||||||
.config/
|
.config/
|
||||||
|
|
||||||
|
# Ignore Jetbrains IntelliJ Workspace Directories
|
||||||
|
.idea/
|
||||||
|
|||||||
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@@ -10,7 +10,7 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"preLaunchTask": "build dotnet",
|
"preLaunchTask": "build dotnet",
|
||||||
// If you have changed target frameworks, make sure to update the program path.
|
// If you have changed target frameworks, make sure to update the program path.
|
||||||
"program": "${workspaceFolder}/_output/net6.0/Radarr",
|
"program": "${workspaceFolder}/_output/net8.0/Radarr",
|
||||||
"args": [],
|
"args": [],
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '5.24.0'
|
majorVersion: '6.0.0'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.427'
|
dotnetVersion: '8.0.405'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.2'
|
innoVersion: '6.2.2'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
@@ -106,7 +106,7 @@ stages:
|
|||||||
echo "Extra platforms already enabled"
|
echo "Extra platforms already enabled"
|
||||||
else
|
else
|
||||||
echo "Enabling extra platform support"
|
echo "Enabling extra platform support"
|
||||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' "$BUNDLEDVERSIONS"
|
||||||
fi
|
fi
|
||||||
displayName: Enable Extra Platform Support
|
displayName: Enable Extra Platform Support
|
||||||
- bash: ./build.sh --backend --enable-extra-platforms
|
- bash: ./build.sh --backend --enable-extra-platforms
|
||||||
@@ -122,27 +122,23 @@ stages:
|
|||||||
artifact: '$(osName)Backend'
|
artifact: '$(osName)Backend'
|
||||||
displayName: Publish Backend
|
displayName: Publish Backend
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
- publish: '$(testsFolder)/net8.0/win-x64/publish'
|
||||||
artifact: win-x64-tests
|
artifact: win-x64-tests
|
||||||
displayName: Publish win-x64 Test Package
|
displayName: Publish win-x64 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
- publish: '$(testsFolder)/net8.0/linux-x64/publish'
|
||||||
artifact: linux-x64-tests
|
artifact: linux-x64-tests
|
||||||
displayName: Publish linux-x64 Test Package
|
displayName: Publish linux-x64 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
|
- publish: '$(testsFolder)/net8.0/linux-musl-x64/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: linux-musl-x64-tests
|
artifact: linux-musl-x64-tests
|
||||||
displayName: Publish linux-musl-x64 Test Package
|
displayName: Publish linux-musl-x64 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
- publish: '$(testsFolder)/net8.0/freebsd-x64/publish'
|
||||||
artifact: freebsd-x64-tests
|
artifact: freebsd-x64-tests
|
||||||
displayName: Publish freebsd-x64 Test Package
|
displayName: Publish freebsd-x64 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
- publish: '$(testsFolder)/net8.0/osx-x64/publish'
|
||||||
artifact: osx-x64-tests
|
artifact: osx-x64-tests
|
||||||
displayName: Publish osx-x64 Test Package
|
displayName: Publish osx-x64 Test Package
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
@@ -189,7 +185,7 @@ stages:
|
|||||||
artifact: '$(osName)Frontend'
|
artifact: '$(osName)Frontend'
|
||||||
displayName: Publish Frontend
|
displayName: Publish Frontend
|
||||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||||
|
|
||||||
- stage: Installer
|
- stage: Installer
|
||||||
dependsOn:
|
dependsOn:
|
||||||
- Build_Backend
|
- Build_Backend
|
||||||
@@ -260,21 +256,21 @@ stages:
|
|||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/win-x64/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create win-x86 zip
|
displayName: Create win-x86 zip
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
|
rootFolderOrFile: $(artifactsFolder)/win-x86/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create osx-x64 app
|
displayName: Create osx-x64 app
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create osx-x64 tar
|
displayName: Create osx-x64 tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -282,14 +278,14 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-x64/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create osx-arm64 app
|
displayName: Create osx-arm64 app
|
||||||
inputs:
|
inputs:
|
||||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-arm64.zip'
|
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-arm64.zip'
|
||||||
archiveType: 'zip'
|
archiveType: 'zip'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create osx-arm64 tar
|
displayName: Create osx-arm64 tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -297,7 +293,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-x64 tar
|
displayName: Create linux-x64 tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -305,7 +301,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-x64/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-musl-x64 tar
|
displayName: Create linux-musl-x64 tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -313,15 +309,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net8.0
|
||||||
- task: ArchiveFiles@2
|
|
||||||
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
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-arm tar
|
displayName: Create linux-arm tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -329,7 +317,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-arm/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-musl-arm tar
|
displayName: Create linux-musl-arm tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -337,7 +325,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-arm64 tar
|
displayName: Create linux-arm64 tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -345,7 +333,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create linux-musl-arm64 tar
|
displayName: Create linux-musl-arm64 tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -353,7 +341,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net8.0
|
||||||
- task: ArchiveFiles@2
|
- task: ArchiveFiles@2
|
||||||
displayName: Create freebsd-x64 tar
|
displayName: Create freebsd-x64 tar
|
||||||
inputs:
|
inputs:
|
||||||
@@ -361,7 +349,7 @@ stages:
|
|||||||
archiveType: 'tar'
|
archiveType: 'tar'
|
||||||
tarCompression: 'gz'
|
tarCompression: 'gz'
|
||||||
includeRootFolder: false
|
includeRootFolder: false
|
||||||
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net6.0
|
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net8.0
|
||||||
- publish: $(Build.ArtifactStagingDirectory)
|
- publish: $(Build.ArtifactStagingDirectory)
|
||||||
artifact: 'Packages'
|
artifact: 'Packages'
|
||||||
displayName: Publish Packages
|
displayName: Publish Packages
|
||||||
@@ -392,7 +380,7 @@ stages:
|
|||||||
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
|
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
|
||||||
SENTRY_ORG: $(sentryOrg)
|
SENTRY_ORG: $(sentryOrg)
|
||||||
SENTRY_URL: $(sentryUrl)
|
SENTRY_URL: $(sentryUrl)
|
||||||
|
|
||||||
- stage: Unit_Test
|
- stage: Unit_Test
|
||||||
displayName: Unit Tests
|
displayName: Unit Tests
|
||||||
dependsOn: Build_Backend
|
dependsOn: Build_Backend
|
||||||
@@ -493,29 +481,19 @@ stages:
|
|||||||
testName: 'Musl Net Core'
|
testName: 'Musl Net Core'
|
||||||
artifactName: linux-musl-x64-tests
|
artifactName: linux-musl-x64-tests
|
||||||
containerImage: ghcr.io/servarr/testimages:alpine
|
containerImage: ghcr.io/servarr/testimages:alpine
|
||||||
linux-x86:
|
|
||||||
testName: 'linux-x86'
|
|
||||||
artifactName: linux-x86-tests
|
|
||||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: ${{ variables.linuxImage }}
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
container: $[ variables['containerImage'] ]
|
container: $[ variables['containerImage'] ]
|
||||||
|
|
||||||
timeoutInMinutes: 10
|
timeoutInMinutes: 10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .NET'
|
displayName: 'Install .NET'
|
||||||
inputs:
|
inputs:
|
||||||
version: $(dotnetVersion)
|
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
|
- checkout: none
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
@@ -559,7 +537,7 @@ stages:
|
|||||||
vmImage: ${{ variables.linuxImage }}
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
timeoutInMinutes: 10
|
timeoutInMinutes: 10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .net core'
|
displayName: 'Install .net core'
|
||||||
@@ -611,12 +589,12 @@ stages:
|
|||||||
Radarr__Postgres__Port: '5432'
|
Radarr__Postgres__Port: '5432'
|
||||||
Radarr__Postgres__User: 'radarr'
|
Radarr__Postgres__User: 'radarr'
|
||||||
Radarr__Postgres__Password: 'radarr'
|
Radarr__Postgres__Password: 'radarr'
|
||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: ${{ variables.linuxImage }}
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
timeoutInMinutes: 10
|
timeoutInMinutes: 10
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .net core'
|
displayName: 'Install .net core'
|
||||||
@@ -699,7 +677,7 @@ stages:
|
|||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .net core'
|
displayName: 'Install .net core'
|
||||||
@@ -721,7 +699,7 @@ stages:
|
|||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- task: ExtractFiles@1
|
- task: ExtractFiles@1
|
||||||
inputs:
|
inputs:
|
||||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||||
displayName: Extract Package
|
displayName: Extract Package
|
||||||
- bash: |
|
- bash: |
|
||||||
@@ -776,7 +754,7 @@ stages:
|
|||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- task: ExtractFiles@1
|
- task: ExtractFiles@1
|
||||||
inputs:
|
inputs:
|
||||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||||
displayName: Extract Package
|
displayName: Extract Package
|
||||||
- bash: |
|
- bash: |
|
||||||
@@ -840,7 +818,7 @@ stages:
|
|||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- task: ExtractFiles@1
|
- task: ExtractFiles@1
|
||||||
inputs:
|
inputs:
|
||||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||||
displayName: Extract Package
|
displayName: Extract Package
|
||||||
- bash: |
|
- bash: |
|
||||||
@@ -926,29 +904,18 @@ stages:
|
|||||||
artifactName: linux-musl-x64-tests
|
artifactName: linux-musl-x64-tests
|
||||||
containerImage: ghcr.io/servarr/testimages:alpine
|
containerImage: ghcr.io/servarr/testimages:alpine
|
||||||
pattern: 'Radarr.*.linux-musl-core-x64.tar.gz'
|
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:
|
pool:
|
||||||
vmImage: ${{ variables.linuxImage }}
|
vmImage: ${{ variables.linuxImage }}
|
||||||
|
|
||||||
container: $[ variables['containerImage'] ]
|
container: $[ variables['containerImage'] ]
|
||||||
|
|
||||||
timeoutInMinutes: 15
|
timeoutInMinutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .NET'
|
displayName: 'Install .NET'
|
||||||
inputs:
|
inputs:
|
||||||
version: $(dotnetVersion)
|
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
|
- checkout: none
|
||||||
- task: DownloadPipelineArtifact@2
|
- task: DownloadPipelineArtifact@2
|
||||||
displayName: Download Test Artifact
|
displayName: Download Test Artifact
|
||||||
@@ -965,7 +932,7 @@ stages:
|
|||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- task: ExtractFiles@1
|
- task: ExtractFiles@1
|
||||||
inputs:
|
inputs:
|
||||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||||
displayName: Extract Package
|
displayName: Extract Package
|
||||||
- bash: |
|
- bash: |
|
||||||
@@ -988,7 +955,7 @@ stages:
|
|||||||
- stage: Automation
|
- stage: Automation
|
||||||
displayName: Automation
|
displayName: Automation
|
||||||
dependsOn: Packages
|
dependsOn: Packages
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Automation
|
- job: Automation
|
||||||
strategy:
|
strategy:
|
||||||
@@ -1014,7 +981,7 @@ stages:
|
|||||||
|
|
||||||
pool:
|
pool:
|
||||||
vmImage: $(imageName)
|
vmImage: $(imageName)
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- task: UseDotNet@2
|
- task: UseDotNet@2
|
||||||
displayName: 'Install .net core'
|
displayName: 'Install .net core'
|
||||||
@@ -1036,7 +1003,7 @@ stages:
|
|||||||
targetPath: $(Build.ArtifactStagingDirectory)
|
targetPath: $(Build.ArtifactStagingDirectory)
|
||||||
- task: ExtractFiles@1
|
- task: ExtractFiles@1
|
||||||
inputs:
|
inputs:
|
||||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||||
displayName: Extract Package
|
displayName: Extract Package
|
||||||
- bash: |
|
- bash: |
|
||||||
@@ -1161,7 +1128,7 @@ stages:
|
|||||||
- checkout: self
|
- checkout: self
|
||||||
submodules: true
|
submodules: true
|
||||||
persistCredentials: true
|
persistCredentials: true
|
||||||
fetchDepth: 1
|
fetchDepth: 1
|
||||||
- bash: ./docs.sh Windows
|
- bash: ./docs.sh Windows
|
||||||
displayName: Create openapi.json
|
displayName: Create openapi.json
|
||||||
- bash: |
|
- bash: |
|
||||||
@@ -1230,13 +1197,13 @@ stages:
|
|||||||
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
|
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
|
||||||
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
|
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
|
||||||
- bash: |
|
- bash: |
|
||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net8.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net8.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@3
|
- task: SonarCloudAnalyze@3
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@5.3.11
|
- task: reportgenerator@5
|
||||||
displayName: Generate Coverage Report
|
displayName: Generate Coverage Report
|
||||||
inputs:
|
inputs:
|
||||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||||
@@ -1274,4 +1241,3 @@ stages:
|
|||||||
DISCORDCHANNELID: $(discordChannelId)
|
DISCORDCHANNELID: $(discordChannelId)
|
||||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||||
DISCORDTHREADID: $(discordThreadId)
|
DISCORDTHREADID: $(discordThreadId)
|
||||||
|
|
||||||
|
|||||||
52
build.sh
52
build.sh
@@ -33,14 +33,14 @@ EnableExtraPlatformsInSDK()
|
|||||||
echo "Extra platforms already enabled"
|
echo "Extra platforms already enabled"
|
||||||
else
|
else
|
||||||
echo "Enabling extra platform support"
|
echo "Enabling extra platform support"
|
||||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' "$BUNDLEDVERSIONS"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
EnableExtraPlatforms()
|
EnableExtraPlatforms()
|
||||||
{
|
{
|
||||||
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
||||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
|
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,9 +79,9 @@ Build()
|
|||||||
|
|
||||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||||
then
|
then
|
||||||
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform=$platform -t:PublishAllRids
|
dotnet msbuild -restore $slnFile -p:SelfContained=True -p:Configuration=Release -p:Platform=$platform -t:PublishAllRids
|
||||||
else
|
else
|
||||||
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform=$platform -p:RuntimeIdentifiers=$RID -t:PublishAllRids
|
dotnet msbuild -restore $slnFile -p:SelfContained=True -p:Configuration=Release -p:Platform=$platform -p:RuntimeIdentifiers=$RID -t:PublishAllRids
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ProgressEnd 'Build'
|
ProgressEnd 'Build'
|
||||||
@@ -137,7 +137,7 @@ PackageLinux()
|
|||||||
|
|
||||||
echo "Adding Radarr.Mono to UpdatePackage"
|
echo "Adding Radarr.Mono to UpdatePackage"
|
||||||
cp $folder/Radarr.Mono.* $folder/Radarr.Update
|
cp $folder/Radarr.Mono.* $folder/Radarr.Update
|
||||||
if [ "$framework" = "net6.0" ]; then
|
if [ "$framework" = "net8.0" ]; then
|
||||||
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
|
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
|
||||||
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
|
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
|
||||||
fi
|
fi
|
||||||
@@ -165,7 +165,7 @@ PackageMacOS()
|
|||||||
|
|
||||||
echo "Adding Radarr.Mono to UpdatePackage"
|
echo "Adding Radarr.Mono to UpdatePackage"
|
||||||
cp $folder/Radarr.Mono.* $folder/Radarr.Update
|
cp $folder/Radarr.Mono.* $folder/Radarr.Update
|
||||||
if [ "$framework" = "net6.0" ]; then
|
if [ "$framework" = "net8.0" ]; then
|
||||||
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
|
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
|
||||||
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
|
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
|
||||||
fi
|
fi
|
||||||
@@ -377,15 +377,14 @@ then
|
|||||||
Build
|
Build
|
||||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||||
then
|
then
|
||||||
PackageTests "net6.0" "win-x64"
|
PackageTests "net8.0" "win-x64"
|
||||||
PackageTests "net6.0" "win-x86"
|
PackageTests "net8.0" "win-x86"
|
||||||
PackageTests "net6.0" "linux-x64"
|
PackageTests "net8.0" "linux-x64"
|
||||||
PackageTests "net6.0" "linux-musl-x64"
|
PackageTests "net8.0" "linux-musl-x64"
|
||||||
PackageTests "net6.0" "osx-x64"
|
PackageTests "net8.0" "osx-x64"
|
||||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||||
then
|
then
|
||||||
PackageTests "net6.0" "freebsd-x64"
|
PackageTests "net8.0" "freebsd-x64"
|
||||||
PackageTests "net6.0" "linux-x86"
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
PackageTests "$FRAMEWORK" "$RID"
|
PackageTests "$FRAMEWORK" "$RID"
|
||||||
@@ -413,20 +412,19 @@ then
|
|||||||
|
|
||||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||||
then
|
then
|
||||||
Package "net6.0" "win-x64"
|
Package "net8.0" "win-x64"
|
||||||
Package "net6.0" "win-x86"
|
Package "net8.0" "win-x86"
|
||||||
Package "net6.0" "linux-x64"
|
Package "net8.0" "linux-x64"
|
||||||
Package "net6.0" "linux-musl-x64"
|
Package "net8.0" "linux-musl-x64"
|
||||||
Package "net6.0" "linux-arm64"
|
Package "net8.0" "linux-arm64"
|
||||||
Package "net6.0" "linux-musl-arm64"
|
Package "net8.0" "linux-musl-arm64"
|
||||||
Package "net6.0" "linux-arm"
|
Package "net8.0" "linux-arm"
|
||||||
Package "net6.0" "linux-musl-arm"
|
Package "net8.0" "linux-musl-arm"
|
||||||
Package "net6.0" "osx-x64"
|
Package "net8.0" "osx-x64"
|
||||||
Package "net6.0" "osx-arm64"
|
Package "net8.0" "osx-arm64"
|
||||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||||
then
|
then
|
||||||
Package "net6.0" "freebsd-x64"
|
Package "net8.0" "freebsd-x64"
|
||||||
Package "net6.0" "linux-x86"
|
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
Package "$FRAMEWORK" "$RID"
|
Package "$FRAMEWORK" "$RID"
|
||||||
@@ -436,7 +434,7 @@ fi
|
|||||||
if [ "$INSTALLER" = "YES" ];
|
if [ "$INSTALLER" = "YES" ];
|
||||||
then
|
then
|
||||||
InstallInno
|
InstallInno
|
||||||
BuildInstaller "net6.0" "win-x64"
|
BuildInstaller "net8.0" "win-x64"
|
||||||
BuildInstaller "net6.0" "win-x86"
|
BuildInstaller "net8.0" "win-x86"
|
||||||
RemoveInno
|
RemoveInno
|
||||||
fi
|
fi
|
||||||
|
|||||||
4
docs.sh
4
docs.sh
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
FRAMEWORK="net6.0"
|
FRAMEWORK="net8.0"
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
ARCHITECTURE="${2:-x64}"
|
ARCHITECTURE="${2:-x64}"
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ dotnet clean $slnFile -c Release
|
|||||||
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
|
||||||
|
|
||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 8.1.4 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
|
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ function BlocklistDetailsModal(props: BlocklistDetailsModalProps) {
|
|||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>Details</ModalHeader>
|
<ModalHeader>{translate('Details')}</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ function Queue() {
|
|||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
<PageToolbarSection>
|
<PageToolbarSection>
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Refresh"
|
label={translate('Refresh')}
|
||||||
iconName={icons.REFRESH}
|
iconName={icons.REFRESH}
|
||||||
isSpinning={isRefreshing}
|
isSpinning={isRefreshing}
|
||||||
onPress={handleRefreshPress}
|
onPress={handleRefreshPress}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ function QueueStatus(props: QueueStatusProps) {
|
|||||||
|
|
||||||
if (trackedDownloadState === 'importing') {
|
if (trackedDownloadState === 'importing') {
|
||||||
title += ` - ${translate('Importing')}`;
|
title += ` - ${translate('Importing')}`;
|
||||||
iconKind = kinds.PURPLE;
|
iconKind = kinds.PRIMARY;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDownloadState === 'failedPending') {
|
if (trackedDownloadState === 'failedPending') {
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ class ImportMovie extends Component {
|
|||||||
{
|
{
|
||||||
!rootFoldersFetching && !!rootFoldersError ?
|
!rootFoldersFetching && !!rootFoldersError ?
|
||||||
<Alert kind={kinds.DANGER}>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('UnableToLoadRootFolders')}
|
{translate('RootFoldersLoadError')}
|
||||||
</Alert> :
|
</Alert> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ class ImportMovieSelectFolder extends Component {
|
|||||||
{
|
{
|
||||||
!isFetching && error ?
|
!isFetching && error ?
|
||||||
<Alert kind={kinds.DANGER}>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('UnableToLoadRootFolders')}
|
{translate('RootFoldersLoadError')}
|
||||||
</Alert> :
|
</Alert> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import History from 'Activity/History/History';
|
|||||||
import Queue from 'Activity/Queue/Queue';
|
import Queue from 'Activity/Queue/Queue';
|
||||||
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
||||||
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
||||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
import CalendarPage from 'Calendar/CalendarPage';
|
||||||
import CollectionConnector from 'Collection/CollectionConnector';
|
import CollectionConnector from 'Collection/CollectionConnector';
|
||||||
import NotFound from 'Components/NotFound';
|
import NotFound from 'Components/NotFound';
|
||||||
import Switch from 'Components/Router/Switch';
|
import Switch from 'Components/Router/Switch';
|
||||||
@@ -15,9 +15,9 @@ import MovieIndex from 'Movie/Index/MovieIndex';
|
|||||||
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
||||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
import ImportListSettings from 'Settings/ImportLists/ImportListSettings';
|
||||||
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
|
||||||
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
import MediaManagement from 'Settings/MediaManagement/MediaManagement';
|
||||||
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
||||||
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
||||||
import Profiles from 'Settings/Profiles/Profiles';
|
import Profiles from 'Settings/Profiles/Profiles';
|
||||||
@@ -73,7 +73,7 @@ function AppRoutes() {
|
|||||||
Calendar
|
Calendar
|
||||||
*/}
|
*/}
|
||||||
|
|
||||||
<Route path="/calendar" component={CalendarPageConnector} />
|
<Route path="/calendar" component={CalendarPage} />
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
Activity
|
Activity
|
||||||
@@ -99,10 +99,7 @@ function AppRoutes() {
|
|||||||
|
|
||||||
<Route exact={true} path="/settings" component={Settings} />
|
<Route exact={true} path="/settings" component={Settings} />
|
||||||
|
|
||||||
<Route
|
<Route path="/settings/mediamanagement" component={MediaManagement} />
|
||||||
path="/settings/mediamanagement"
|
|
||||||
component={MediaManagementConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route path="/settings/profiles" component={Profiles} />
|
<Route path="/settings/profiles" component={Profiles} />
|
||||||
|
|
||||||
@@ -113,17 +110,14 @@ function AppRoutes() {
|
|||||||
component={CustomFormatSettingsPage}
|
component={CustomFormatSettingsPage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path="/settings/indexers" component={IndexerSettingsConnector} />
|
<Route path="/settings/indexers" component={IndexerSettings} />
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/settings/downloadclients"
|
path="/settings/downloadclients"
|
||||||
component={DownloadClientSettingsConnector}
|
component={DownloadClientSettingsConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route path="/settings/importlists" component={ImportListSettings} />
|
||||||
path="/settings/importlists"
|
|
||||||
component={ImportListSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route path="/settings/connect" component={NotificationSettings} />
|
<Route path="/settings/connect" component={NotificationSettings} />
|
||||||
|
|
||||||
|
|||||||
@@ -43,9 +43,15 @@ export interface AppSectionSchemaState<T> {
|
|||||||
isSchemaFetching: boolean;
|
isSchemaFetching: boolean;
|
||||||
isSchemaPopulated: boolean;
|
isSchemaPopulated: boolean;
|
||||||
schemaError: Error;
|
schemaError: Error;
|
||||||
schema: {
|
schema: T[];
|
||||||
items: T[];
|
selectedSchema?: T;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export interface AppSectionItemSchemaState<T> {
|
||||||
|
isSchemaFetching: boolean;
|
||||||
|
isSchemaPopulated: boolean;
|
||||||
|
schemaError: Error;
|
||||||
|
schema: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionItemState<T> {
|
export interface AppSectionItemState<T> {
|
||||||
@@ -61,9 +67,10 @@ export interface AppSectionProviderState<T>
|
|||||||
AppSectionSaveState {
|
AppSectionSaveState {
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isPopulated: boolean;
|
isPopulated: boolean;
|
||||||
|
isTesting?: boolean;
|
||||||
error: Error;
|
error: Error;
|
||||||
items: T[];
|
items: T[];
|
||||||
pendingChanges: Partial<T>;
|
pendingChanges?: Partial<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppSectionState<T> {
|
interface AppSectionState<T> {
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export interface AppSectionState {
|
|||||||
prevVersion?: string;
|
prevVersion?: string;
|
||||||
dimensions: {
|
dimensions: {
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
|
isLargeScreen: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,29 @@
|
|||||||
|
import moment from 'moment';
|
||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionFilterState,
|
AppSectionFilterState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import Movie from 'Movie/Movie';
|
import { CalendarView } from 'Calendar/calendarViews';
|
||||||
|
import { CalendarItem } from 'typings/Calendar';
|
||||||
|
|
||||||
|
interface CalendarOptions {
|
||||||
|
showMovieInformation: boolean;
|
||||||
|
showCinemaRelease: boolean;
|
||||||
|
showDigitalRelease: boolean;
|
||||||
|
showPhysicalRelease: boolean;
|
||||||
|
showCutoffUnmetIcon: boolean;
|
||||||
|
fullColorEvents: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface CalendarAppState
|
interface CalendarAppState
|
||||||
extends AppSectionState<Movie>,
|
extends AppSectionState<CalendarItem>,
|
||||||
AppSectionFilterState<Movie> {}
|
AppSectionFilterState<CalendarItem> {
|
||||||
|
searchMissingCommandId: number | null;
|
||||||
|
start: moment.Moment;
|
||||||
|
end: moment.Moment;
|
||||||
|
dates: string[];
|
||||||
|
time: string;
|
||||||
|
view: CalendarView;
|
||||||
|
options: CalendarOptions;
|
||||||
|
}
|
||||||
|
|
||||||
export default CalendarAppState;
|
export default CalendarAppState;
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
AppSectionItemSchemaState,
|
||||||
AppSectionItemState,
|
AppSectionItemState,
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
AppSectionSchemaState,
|
AppSectionSchemaState,
|
||||||
PagedAppSectionState,
|
PagedAppSectionState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import Language from 'Language/Language';
|
import Language from 'Language/Language';
|
||||||
|
import AutoTagging, { AutoTaggingSpecification } from 'typings/AutoTagging';
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
|
import DelayProfile from 'typings/DelayProfile';
|
||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
import ImportListExclusion from 'typings/ImportListExclusion';
|
import ImportListExclusion from 'typings/ImportListExclusion';
|
||||||
@@ -16,12 +19,34 @@ import IndexerFlag from 'typings/IndexerFlag';
|
|||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import General from 'typings/Settings/General';
|
import General from 'typings/Settings/General';
|
||||||
|
import IndexerOptions from 'typings/Settings/IndexerOptions';
|
||||||
|
import MediaManagement from 'typings/Settings/MediaManagement';
|
||||||
import NamingConfig from 'typings/Settings/NamingConfig';
|
import NamingConfig from 'typings/Settings/NamingConfig';
|
||||||
import NamingExample from 'typings/Settings/NamingExample';
|
import NamingExample from 'typings/Settings/NamingExample';
|
||||||
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
import UiSettings from 'typings/Settings/UiSettings';
|
||||||
import MetadataAppState from './MetadataAppState';
|
import MetadataAppState from './MetadataAppState';
|
||||||
|
|
||||||
|
type Presets<T> = T & {
|
||||||
|
presets: T[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface AutoTaggingAppState
|
||||||
|
extends AppSectionState<AutoTagging>,
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface AutoTaggingSpecificationAppState
|
||||||
|
extends AppSectionState<AutoTaggingSpecification>,
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState,
|
||||||
|
AppSectionSchemaState<AutoTaggingSpecification> {}
|
||||||
|
|
||||||
|
export interface DelayProfileAppState
|
||||||
|
extends AppSectionState<DelayProfile>,
|
||||||
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
@@ -33,6 +58,10 @@ export interface GeneralAppState
|
|||||||
extends AppSectionItemState<General>,
|
extends AppSectionItemState<General>,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface MediaManagementAppState
|
||||||
|
extends AppSectionItemState<MediaManagement>,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export interface NamingAppState
|
export interface NamingAppState
|
||||||
extends AppSectionItemState<NamingConfig>,
|
extends AppSectionItemState<NamingConfig>,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
@@ -42,12 +71,20 @@ export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
|
|||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
|
AppSectionSaveState,
|
||||||
|
AppSectionSchemaState<Presets<ImportList>> {
|
||||||
|
isTestingAll: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IndexerOptionsAppState
|
||||||
|
extends AppSectionItemState<IndexerOptions>,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export interface IndexerAppState
|
export interface IndexerAppState
|
||||||
extends AppSectionState<Indexer>,
|
extends AppSectionState<Indexer>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState {
|
AppSectionSaveState,
|
||||||
|
AppSectionSchemaState<Presets<Indexer>> {
|
||||||
isTestingAll: boolean;
|
isTestingAll: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +94,7 @@ export interface NotificationAppState
|
|||||||
|
|
||||||
export interface QualityProfilesAppState
|
export interface QualityProfilesAppState
|
||||||
extends AppSectionState<QualityProfile>,
|
extends AppSectionState<QualityProfile>,
|
||||||
AppSectionSchemaState<QualityProfile> {}
|
AppSectionItemSchemaState<QualityProfile> {}
|
||||||
|
|
||||||
export interface ReleaseProfilesAppState
|
export interface ReleaseProfilesAppState
|
||||||
extends AppSectionState<ReleaseProfile>,
|
extends AppSectionState<ReleaseProfile>,
|
||||||
@@ -88,15 +125,20 @@ export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
|||||||
|
|
||||||
interface SettingsAppState {
|
interface SettingsAppState {
|
||||||
advancedSettings: boolean;
|
advancedSettings: boolean;
|
||||||
|
autoTaggings: AutoTaggingAppState;
|
||||||
|
autoTaggingSpecifications: AutoTaggingSpecificationAppState;
|
||||||
customFormats: CustomFormatAppState;
|
customFormats: CustomFormatAppState;
|
||||||
|
delayProfiles: DelayProfileAppState;
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
general: GeneralAppState;
|
general: GeneralAppState;
|
||||||
importListExclusions: ImportListExclusionsSettingsAppState;
|
importListExclusions: ImportListExclusionsSettingsAppState;
|
||||||
importListOptions: ImportListOptionsSettingsAppState;
|
importListOptions: ImportListOptionsSettingsAppState;
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
|
indexerOptions: IndexerOptionsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
languages: LanguageSettingsAppState;
|
languages: LanguageSettingsAppState;
|
||||||
|
mediaManagement: MediaManagementAppState;
|
||||||
metadata: MetadataAppState;
|
metadata: MetadataAppState;
|
||||||
naming: NamingAppState;
|
naming: NamingAppState;
|
||||||
namingExamples: NamingExamplesAppState;
|
namingExamples: NamingExamplesAppState;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export interface TagDetail extends ModelBase {
|
|||||||
indexerIds: number[];
|
indexerIds: number[];
|
||||||
movieIds: number[];
|
movieIds: number[];
|
||||||
notificationIds: number[];
|
notificationIds: number[];
|
||||||
restrictionIds: number[];
|
releaseProfileIds: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TagDetailAppState
|
export interface TagDetailAppState
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import AgendaEventConnector from './AgendaEventConnector';
|
|
||||||
import styles from './Agenda.css';
|
|
||||||
|
|
||||||
function Agenda(props) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
start,
|
|
||||||
end
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const startDateParsed = Date.parse(start);
|
|
||||||
const endDateParsed = Date.parse(end);
|
|
||||||
|
|
||||||
items.forEach((item) => {
|
|
||||||
const cinemaDateParsed = Date.parse(item.inCinemas);
|
|
||||||
const digitalDateParsed = Date.parse(item.digitalRelease);
|
|
||||||
const physicalDateParsed = Date.parse(item.physicalRelease);
|
|
||||||
const dates = [];
|
|
||||||
|
|
||||||
if (cinemaDateParsed > 0 && cinemaDateParsed >= startDateParsed && cinemaDateParsed <= endDateParsed) {
|
|
||||||
dates.push(cinemaDateParsed);
|
|
||||||
}
|
|
||||||
if (digitalDateParsed > 0 && digitalDateParsed >= startDateParsed && digitalDateParsed <= endDateParsed) {
|
|
||||||
dates.push(digitalDateParsed);
|
|
||||||
}
|
|
||||||
if (physicalDateParsed > 0 && physicalDateParsed >= startDateParsed && physicalDateParsed <= endDateParsed) {
|
|
||||||
dates.push(physicalDateParsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
item.sortDate = Math.min(...dates);
|
|
||||||
item.cinemaDateParsed = cinemaDateParsed;
|
|
||||||
item.digitalDateParsed = digitalDateParsed;
|
|
||||||
item.physicalDateParsed = physicalDateParsed;
|
|
||||||
});
|
|
||||||
|
|
||||||
items.sort((a, b) => ((a.sortDate > b.sortDate) ? 1 : -1));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.agenda}>
|
|
||||||
{
|
|
||||||
items.map((item, index) => {
|
|
||||||
const momentDate = moment(item.sortDate);
|
|
||||||
const showDate = index === 0 ||
|
|
||||||
!moment(items[index - 1].sortDate).isSame(momentDate, 'day');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AgendaEventConnector
|
|
||||||
key={item.id}
|
|
||||||
movieId={item.id}
|
|
||||||
showDate={showDate}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Agenda.propTypes = {
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
start: PropTypes.string.isRequired,
|
|
||||||
end: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Agenda;
|
|
||||||
81
frontend/src/Calendar/Agenda/Agenda.tsx
Normal file
81
frontend/src/Calendar/Agenda/Agenda.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Movie from 'Movie/Movie';
|
||||||
|
import AgendaEvent from './AgendaEvent';
|
||||||
|
import styles from './Agenda.css';
|
||||||
|
|
||||||
|
interface AgendaMovie extends Movie {
|
||||||
|
sortDate: moment.Moment;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Agenda() {
|
||||||
|
const { start, end, items } = useSelector(
|
||||||
|
(state: AppState) => state.calendar
|
||||||
|
);
|
||||||
|
|
||||||
|
const events = useMemo(() => {
|
||||||
|
const result = items.map((item): AgendaMovie => {
|
||||||
|
const { inCinemas, digitalRelease, physicalRelease } = item;
|
||||||
|
|
||||||
|
const dates = [];
|
||||||
|
|
||||||
|
if (inCinemas) {
|
||||||
|
const inCinemasMoment = moment(inCinemas);
|
||||||
|
|
||||||
|
if (inCinemasMoment.isAfter(start) && inCinemasMoment.isBefore(end)) {
|
||||||
|
dates.push(inCinemasMoment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digitalRelease) {
|
||||||
|
const digitalReleaseMoment = moment(digitalRelease);
|
||||||
|
|
||||||
|
if (
|
||||||
|
digitalReleaseMoment.isAfter(start) &&
|
||||||
|
digitalReleaseMoment.isBefore(end)
|
||||||
|
) {
|
||||||
|
dates.push(digitalReleaseMoment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (physicalRelease) {
|
||||||
|
const physicalReleaseMoment = moment(physicalRelease);
|
||||||
|
|
||||||
|
if (
|
||||||
|
physicalReleaseMoment.isAfter(start) &&
|
||||||
|
physicalReleaseMoment.isBefore(end)
|
||||||
|
) {
|
||||||
|
dates.push(physicalReleaseMoment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortDate = moment.min(...dates);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
sortDate,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
result.sort((a, b) => (a.sortDate > b.sortDate ? 1 : -1));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [items, start, end]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.agenda}>
|
||||||
|
{events.map((item, index) => {
|
||||||
|
const momentDate = moment(item.sortDate);
|
||||||
|
const showDate =
|
||||||
|
index === 0 ||
|
||||||
|
!moment(events[index - 1].sortDate).isSame(momentDate, 'day');
|
||||||
|
|
||||||
|
return <AgendaEvent key={item.id} showDate={showDate} {...item} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Agenda;
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import Agenda from './Agenda';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar,
|
|
||||||
(calendar) => {
|
|
||||||
return calendar;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(Agenda);
|
|
||||||
@@ -53,6 +53,13 @@
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.releaseIcon {
|
||||||
|
margin-right: 20px;
|
||||||
|
width: 25px;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
.statusIcon {
|
.statusIcon {
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
@@ -107,8 +114,3 @@
|
|||||||
flex: 0 0 100%;
|
flex: 0 0 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.releaseIcon {
|
|
||||||
margin-right: 20px;
|
|
||||||
width: 25px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
|
|
||||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './AgendaEvent.css';
|
|
||||||
|
|
||||||
class AgendaEvent extends Component {
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isDetailsModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDetailsModalClose = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
movieFile,
|
|
||||||
title,
|
|
||||||
titleSlug,
|
|
||||||
genres,
|
|
||||||
isAvailable,
|
|
||||||
inCinemas,
|
|
||||||
digitalRelease,
|
|
||||||
physicalRelease,
|
|
||||||
monitored,
|
|
||||||
hasFile,
|
|
||||||
grabbed,
|
|
||||||
queueItem,
|
|
||||||
showDate,
|
|
||||||
showMovieInformation,
|
|
||||||
showCutoffUnmetIcon,
|
|
||||||
longDateFormat,
|
|
||||||
colorImpairedMode,
|
|
||||||
cinemaDateParsed,
|
|
||||||
digitalDateParsed,
|
|
||||||
physicalDateParsed,
|
|
||||||
sortDate
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
let startTime = null;
|
|
||||||
let releaseIcon = null;
|
|
||||||
|
|
||||||
if (physicalDateParsed === sortDate) {
|
|
||||||
startTime = physicalRelease;
|
|
||||||
releaseIcon = icons.DISC;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (digitalDateParsed === sortDate) {
|
|
||||||
startTime = digitalRelease;
|
|
||||||
releaseIcon = icons.MOVIE_FILE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cinemaDateParsed === sortDate) {
|
|
||||||
startTime = inCinemas;
|
|
||||||
releaseIcon = icons.IN_CINEMAS;
|
|
||||||
}
|
|
||||||
|
|
||||||
startTime = moment(startTime);
|
|
||||||
const downloading = !!(queueItem || grabbed);
|
|
||||||
const isMonitored = monitored;
|
|
||||||
const statusStyle = getStatusStyle(hasFile, downloading, isMonitored, isAvailable);
|
|
||||||
const joinedGenres = genres.slice(0, 2).join(', ');
|
|
||||||
const link = `/movie/${titleSlug}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.event}>
|
|
||||||
<Link
|
|
||||||
className={styles.underlay}
|
|
||||||
to={link}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.overlay}>
|
|
||||||
<div className={styles.date}>
|
|
||||||
{showDate ? startTime.format(longDateFormat) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.releaseIcon}>
|
|
||||||
<Icon
|
|
||||||
name={releaseIcon}
|
|
||||||
kind={kinds.DEFAULT}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.eventWrapper,
|
|
||||||
styles[statusStyle],
|
|
||||||
colorImpairedMode && 'colorImpaired'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={styles.movieTitle}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
showMovieInformation &&
|
|
||||||
<div className={styles.genres}>
|
|
||||||
{joinedGenres}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!!queueItem &&
|
|
||||||
<span className={styles.statusIcon}>
|
|
||||||
<CalendarEventQueueDetails
|
|
||||||
{...queueItem}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!queueItem && grabbed &&
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.DOWNLOADING}
|
|
||||||
title={translate('MovieIsDownloading')}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showCutoffUnmetIcon && !!movieFile && movieFile.qualityCutoffNotMet &&
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.MOVIE_FILE}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
title={translate('QualityCutoffNotMet')}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AgendaEvent.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
movieFile: PropTypes.object,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
titleSlug: PropTypes.string.isRequired,
|
|
||||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
|
||||||
inCinemas: PropTypes.string,
|
|
||||||
digitalRelease: PropTypes.string,
|
|
||||||
physicalRelease: PropTypes.string,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
hasFile: PropTypes.bool.isRequired,
|
|
||||||
grabbed: PropTypes.bool,
|
|
||||||
queueItem: PropTypes.object,
|
|
||||||
showDate: PropTypes.bool.isRequired,
|
|
||||||
showMovieInformation: PropTypes.bool.isRequired,
|
|
||||||
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
colorImpairedMode: PropTypes.bool.isRequired,
|
|
||||||
cinemaDateParsed: PropTypes.number,
|
|
||||||
digitalDateParsed: PropTypes.number,
|
|
||||||
physicalDateParsed: PropTypes.number,
|
|
||||||
sortDate: PropTypes.number
|
|
||||||
};
|
|
||||||
|
|
||||||
AgendaEvent.defaultProps = {
|
|
||||||
genres: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AgendaEvent;
|
|
||||||
160
frontend/src/Calendar/Agenda/AgendaEvent.tsx
Normal file
160
frontend/src/Calendar/Agenda/AgendaEvent.tsx
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import moment from 'moment';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
|
||||||
|
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import useMovieFile from 'MovieFile/useMovieFile';
|
||||||
|
import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './AgendaEvent.css';
|
||||||
|
|
||||||
|
interface AgendaEventProps {
|
||||||
|
id: number;
|
||||||
|
movieFileId: number;
|
||||||
|
title: string;
|
||||||
|
titleSlug: string;
|
||||||
|
genres: string[];
|
||||||
|
inCinemas?: string;
|
||||||
|
digitalRelease?: string;
|
||||||
|
physicalRelease?: string;
|
||||||
|
sortDate: moment.Moment;
|
||||||
|
isAvailable: boolean;
|
||||||
|
monitored: boolean;
|
||||||
|
hasFile: boolean;
|
||||||
|
grabbed?: boolean;
|
||||||
|
showDate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AgendaEvent({
|
||||||
|
id,
|
||||||
|
movieFileId,
|
||||||
|
title,
|
||||||
|
titleSlug,
|
||||||
|
genres = [],
|
||||||
|
inCinemas,
|
||||||
|
digitalRelease,
|
||||||
|
physicalRelease,
|
||||||
|
sortDate,
|
||||||
|
isAvailable,
|
||||||
|
monitored: isMonitored,
|
||||||
|
hasFile,
|
||||||
|
grabbed,
|
||||||
|
showDate,
|
||||||
|
}: AgendaEventProps) {
|
||||||
|
const movieFile = useMovieFile(movieFileId);
|
||||||
|
const queueItem = useSelector(createQueueItemSelectorForHook(id));
|
||||||
|
const { longDateFormat, enableColorImpairedMode } = useSelector(
|
||||||
|
createUISettingsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
const { showMovieInformation, showCutoffUnmetIcon } = useSelector(
|
||||||
|
(state: AppState) => state.calendar.options
|
||||||
|
);
|
||||||
|
|
||||||
|
const { eventDate, eventTitle, releaseIcon } = useMemo(() => {
|
||||||
|
if (physicalRelease && sortDate.isSame(moment(physicalRelease), 'day')) {
|
||||||
|
return {
|
||||||
|
eventDate: physicalRelease,
|
||||||
|
eventTitle: translate('PhysicalRelease'),
|
||||||
|
releaseIcon: icons.DISC,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digitalRelease && sortDate.isSame(moment(digitalRelease), 'day')) {
|
||||||
|
return {
|
||||||
|
eventDate: digitalRelease,
|
||||||
|
eventTitle: translate('DigitalRelease'),
|
||||||
|
releaseIcon: icons.MOVIE_FILE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inCinemas && sortDate.isSame(moment(inCinemas), 'day')) {
|
||||||
|
return {
|
||||||
|
eventDate: inCinemas,
|
||||||
|
eventTitle: translate('InCinemas'),
|
||||||
|
releaseIcon: icons.IN_CINEMAS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventDate: null,
|
||||||
|
eventTitle: null,
|
||||||
|
releaseIcon: null,
|
||||||
|
};
|
||||||
|
}, [inCinemas, digitalRelease, physicalRelease, sortDate]);
|
||||||
|
|
||||||
|
const downloading = !!(queueItem || grabbed);
|
||||||
|
const statusStyle = getStatusStyle(
|
||||||
|
hasFile,
|
||||||
|
downloading,
|
||||||
|
isMonitored,
|
||||||
|
isAvailable
|
||||||
|
);
|
||||||
|
const joinedGenres = genres.slice(0, 2).join(', ');
|
||||||
|
const link = `/movie/${titleSlug}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.event}>
|
||||||
|
<Link className={styles.underlay} to={link} />
|
||||||
|
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
<div className={styles.date}>
|
||||||
|
{showDate && eventDate
|
||||||
|
? moment(eventDate).format(longDateFormat)
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.releaseIcon}>
|
||||||
|
{releaseIcon ? (
|
||||||
|
<Icon name={releaseIcon} kind={kinds.DEFAULT} title={eventTitle} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.eventWrapper,
|
||||||
|
styles[statusStyle],
|
||||||
|
enableColorImpairedMode && 'colorImpaired'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.movieTitle}>{title}</div>
|
||||||
|
|
||||||
|
{showMovieInformation ? (
|
||||||
|
<div className={styles.genres}>{joinedGenres}</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{queueItem ? (
|
||||||
|
<span className={styles.statusIcon}>
|
||||||
|
<CalendarEventQueueDetails {...queueItem} />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!queueItem && grabbed ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.DOWNLOADING}
|
||||||
|
title={translate('MovieIsDownloading')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showCutoffUnmetIcon && movieFile && movieFile.qualityCutoffNotMet ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.MOVIE_FILE}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
title={translate('QualityCutoffNotMet')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AgendaEvent;
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
|
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
|
||||||
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import AgendaEvent from './AgendaEvent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar.options,
|
|
||||||
createMovieSelector(),
|
|
||||||
createMovieFileSelector(),
|
|
||||||
createQueueItemSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(calendarOptions, movie, movieFile, queueItem, uiSettings) => {
|
|
||||||
return {
|
|
||||||
movie,
|
|
||||||
movieFile,
|
|
||||||
queueItem,
|
|
||||||
...calendarOptions,
|
|
||||||
timeFormat: uiSettings.timeFormat,
|
|
||||||
longDateFormat: uiSettings.longDateFormat,
|
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(AgendaEvent);
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import AgendaConnector from './Agenda/AgendaConnector';
|
|
||||||
import * as calendarViews from './calendarViews';
|
|
||||||
import CalendarDaysConnector from './Day/CalendarDaysConnector';
|
|
||||||
import DaysOfWeekConnector from './Day/DaysOfWeekConnector';
|
|
||||||
import CalendarHeaderConnector from './Header/CalendarHeaderConnector';
|
|
||||||
import styles from './Calendar.css';
|
|
||||||
|
|
||||||
class Calendar extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
view
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.calendar}>
|
|
||||||
{
|
|
||||||
isFetching && !isPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<Alert kind={kinds.DANGER}>{translate('CalendarLoadError')}</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!error && isPopulated && view === calendarViews.AGENDA &&
|
|
||||||
<div className={styles.calendarContent}>
|
|
||||||
<CalendarHeaderConnector />
|
|
||||||
<AgendaConnector />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!error && isPopulated && view !== calendarViews.AGENDA &&
|
|
||||||
<div className={styles.calendarContent}>
|
|
||||||
<CalendarHeaderConnector />
|
|
||||||
<DaysOfWeekConnector />
|
|
||||||
<CalendarDaysConnector />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Calendar.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
view: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Calendar;
|
|
||||||
164
frontend/src/Calendar/Calendar.tsx
Normal file
164
frontend/src/Calendar/Calendar.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import Movie from 'Movie/Movie';
|
||||||
|
import {
|
||||||
|
clearCalendar,
|
||||||
|
fetchCalendar,
|
||||||
|
gotoCalendarToday,
|
||||||
|
} from 'Store/Actions/calendarActions';
|
||||||
|
import {
|
||||||
|
clearMovieFiles,
|
||||||
|
fetchMovieFiles,
|
||||||
|
} from 'Store/Actions/movieFileActions';
|
||||||
|
import {
|
||||||
|
clearQueueDetails,
|
||||||
|
fetchQueueDetails,
|
||||||
|
} from 'Store/Actions/queueActions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
import {
|
||||||
|
registerPagePopulator,
|
||||||
|
unregisterPagePopulator,
|
||||||
|
} from 'Utilities/pagePopulator';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import Agenda from './Agenda/Agenda';
|
||||||
|
import CalendarDays from './Day/CalendarDays';
|
||||||
|
import DaysOfWeek from './Day/DaysOfWeek';
|
||||||
|
import CalendarHeader from './Header/CalendarHeader';
|
||||||
|
import styles from './Calendar.css';
|
||||||
|
|
||||||
|
const UPDATE_DELAY = 3600000; // 1 hour
|
||||||
|
|
||||||
|
function Calendar() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const requestCurrentPage = useCurrentPage();
|
||||||
|
const updateTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
const { isFetching, isPopulated, error, items, time, view } = useSelector(
|
||||||
|
(state: AppState) => state.calendar
|
||||||
|
);
|
||||||
|
|
||||||
|
const isRefreshingMovie = useSelector(
|
||||||
|
createCommandExecutingSelector(commandNames.REFRESH_MOVIE)
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstDayOfWeek = useSelector(
|
||||||
|
(state: AppState) => state.settings.ui.item.firstDayOfWeek
|
||||||
|
);
|
||||||
|
|
||||||
|
const wasRefreshingMovie = usePrevious(isRefreshingMovie);
|
||||||
|
const previousFirstDayOfWeek = usePrevious(firstDayOfWeek);
|
||||||
|
const previousItems = usePrevious(items);
|
||||||
|
|
||||||
|
const handleScheduleUpdate = useCallback(() => {
|
||||||
|
clearTimeout(updateTimeout.current);
|
||||||
|
|
||||||
|
function updateCalendar() {
|
||||||
|
dispatch(gotoCalendarToday());
|
||||||
|
updateTimeout.current = setTimeout(updateCalendar, UPDATE_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimeout.current = setTimeout(updateCalendar, UPDATE_DELAY);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleScheduleUpdate();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(clearCalendar());
|
||||||
|
dispatch(clearQueueDetails());
|
||||||
|
dispatch(clearMovieFiles());
|
||||||
|
clearTimeout(updateTimeout.current);
|
||||||
|
};
|
||||||
|
}, [dispatch, handleScheduleUpdate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestCurrentPage) {
|
||||||
|
dispatch(fetchCalendar());
|
||||||
|
} else {
|
||||||
|
dispatch(gotoCalendarToday());
|
||||||
|
}
|
||||||
|
}, [requestCurrentPage, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const repopulate = () => {
|
||||||
|
dispatch(fetchQueueDetails({ time, view }));
|
||||||
|
dispatch(fetchCalendar({ time, view }));
|
||||||
|
};
|
||||||
|
|
||||||
|
registerPagePopulator(repopulate, ['movieFileUpdated', 'movieFileDeleted']);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregisterPagePopulator(repopulate);
|
||||||
|
};
|
||||||
|
}, [time, view, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleScheduleUpdate();
|
||||||
|
}, [time, handleScheduleUpdate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
previousFirstDayOfWeek != null &&
|
||||||
|
firstDayOfWeek !== previousFirstDayOfWeek
|
||||||
|
) {
|
||||||
|
dispatch(fetchCalendar({ time, view }));
|
||||||
|
}
|
||||||
|
}, [time, view, firstDayOfWeek, previousFirstDayOfWeek, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasRefreshingMovie && !isRefreshingMovie) {
|
||||||
|
dispatch(fetchCalendar({ time, view }));
|
||||||
|
}
|
||||||
|
}, [time, view, isRefreshingMovie, wasRefreshingMovie, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!previousItems || hasDifferentItems(items, previousItems)) {
|
||||||
|
const movieIds = selectUniqueIds<Movie, number>(items, 'id');
|
||||||
|
const movieFileIds = selectUniqueIds<Movie, number>(items, 'movieFileId');
|
||||||
|
|
||||||
|
if (items.length) {
|
||||||
|
dispatch(fetchQueueDetails({ movieIds }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movieFileIds.length) {
|
||||||
|
dispatch(fetchMovieFiles({ movieFileIds }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [items, previousItems, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.calendar}>
|
||||||
|
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetching && error ? (
|
||||||
|
<Alert kind={kinds.DANGER}>{translate('CalendarLoadError')}</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!error && isPopulated && view === 'agenda' ? (
|
||||||
|
<div className={styles.calendarContent}>
|
||||||
|
<CalendarHeader />
|
||||||
|
<Agenda />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!error && isPopulated && view !== 'agenda' ? (
|
||||||
|
<div className={styles.calendarContent}>
|
||||||
|
<CalendarHeader />
|
||||||
|
<DaysOfWeek />
|
||||||
|
<CalendarDays />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Calendar;
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
import * as calendarActions from 'Store/Actions/calendarActions';
|
|
||||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
|
||||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
|
||||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
|
||||||
import Calendar from './Calendar';
|
|
||||||
|
|
||||||
const UPDATE_DELAY = 3600000; // 1 hour
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar,
|
|
||||||
(state) => state.settings.ui.item.firstDayOfWeek,
|
|
||||||
createCommandExecutingSelector(commandNames.REFRESH_MOVIE),
|
|
||||||
(calendar, firstDayOfWeek, isRefreshingMovie) => {
|
|
||||||
return {
|
|
||||||
...calendar,
|
|
||||||
isRefreshingMovie,
|
|
||||||
firstDayOfWeek
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
...calendarActions,
|
|
||||||
fetchMovieFiles,
|
|
||||||
clearMovieFiles,
|
|
||||||
fetchQueueDetails,
|
|
||||||
clearQueueDetails
|
|
||||||
};
|
|
||||||
|
|
||||||
class CalendarConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.updateTimeoutId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
useCurrentPage,
|
|
||||||
fetchCalendar,
|
|
||||||
gotoCalendarToday
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
|
|
||||||
|
|
||||||
if (useCurrentPage) {
|
|
||||||
fetchCalendar();
|
|
||||||
} else {
|
|
||||||
gotoCalendarToday();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scheduleUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
time,
|
|
||||||
view,
|
|
||||||
isRefreshingMovie,
|
|
||||||
firstDayOfWeek
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (hasDifferentItems(prevProps.items, items)) {
|
|
||||||
const movieFileIds = selectUniqueIds(items, 'movieFileId');
|
|
||||||
|
|
||||||
if (movieFileIds.length) {
|
|
||||||
this.props.fetchMovieFiles({ movieFileIds });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (items.length) {
|
|
||||||
this.props.fetchQueueDetails();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.time !== time) {
|
|
||||||
this.scheduleUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.firstDayOfWeek !== firstDayOfWeek) {
|
|
||||||
this.props.fetchCalendar({ time, view });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.isRefreshingMovie && !isRefreshingMovie) {
|
|
||||||
this.props.fetchCalendar({ time, view });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
unregisterPagePopulator(this.repopulate);
|
|
||||||
this.props.clearCalendar();
|
|
||||||
this.props.clearQueueDetails();
|
|
||||||
this.props.clearMovieFiles();
|
|
||||||
this.clearUpdateTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
repopulate = () => {
|
|
||||||
const {
|
|
||||||
time,
|
|
||||||
view
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Calendar
|
|
||||||
{...this.props}
|
|
||||||
onCalendarViewChange={this.onCalendarViewChange}
|
|
||||||
onTodayPress={this.onTodayPress}
|
|
||||||
onPreviousPress={this.onPreviousPress}
|
|
||||||
onNextPress={this.onNextPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarConnector.propTypes = {
|
|
||||||
useCurrentPage: PropTypes.bool.isRequired,
|
|
||||||
time: PropTypes.string,
|
|
||||||
view: PropTypes.string.isRequired,
|
|
||||||
firstDayOfWeek: PropTypes.number.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isRefreshingMovie: PropTypes.bool.isRequired,
|
|
||||||
setCalendarView: PropTypes.func.isRequired,
|
|
||||||
gotoCalendarToday: PropTypes.func.isRequired,
|
|
||||||
gotoCalendarPreviousRange: PropTypes.func.isRequired,
|
|
||||||
gotoCalendarNextRange: PropTypes.func.isRequired,
|
|
||||||
clearCalendar: PropTypes.func.isRequired,
|
|
||||||
fetchCalendar: PropTypes.func.isRequired,
|
|
||||||
fetchMovieFiles: PropTypes.func.isRequired,
|
|
||||||
clearMovieFiles: PropTypes.func.isRequired,
|
|
||||||
fetchQueueDetails: PropTypes.func.isRequired,
|
|
||||||
clearQueueDetails: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarConnector);
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import Measure from 'Components/Measure';
|
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
|
||||||
import { align, icons } from 'Helpers/Props';
|
|
||||||
import NoMovie from 'Movie/NoMovie';
|
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import CalendarConnector from './CalendarConnector';
|
|
||||||
import CalendarFilterModal from './CalendarFilterModal';
|
|
||||||
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
|
||||||
import LegendConnector from './Legend/LegendConnector';
|
|
||||||
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
|
||||||
import styles from './CalendarPage.css';
|
|
||||||
|
|
||||||
const MINIMUM_DAY_WIDTH = 120;
|
|
||||||
|
|
||||||
class CalendarPage extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isCalendarLinkModalOpen: false,
|
|
||||||
isOptionsModalOpen: false,
|
|
||||||
width: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onMeasure = ({ width }) => {
|
|
||||||
this.setState({ width });
|
|
||||||
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 {
|
|
||||||
missingMovieIds,
|
|
||||||
onSearchMissingPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onSearchMissingPress(missingMovieIds);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
hasMovie,
|
|
||||||
movieError,
|
|
||||||
movieIsFetching,
|
|
||||||
movieIsPopulated,
|
|
||||||
missingMovieIds,
|
|
||||||
customFilters,
|
|
||||||
isRssSyncExecuting,
|
|
||||||
isSearchingForMissing,
|
|
||||||
useCurrentPage,
|
|
||||||
onRssSyncPress,
|
|
||||||
onFilterSelect
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isCalendarLinkModalOpen,
|
|
||||||
isOptionsModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const isMeasured = this.state.width > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('Calendar')}>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('ICalLink')}
|
|
||||||
iconName={icons.CALENDAR}
|
|
||||||
onPress={this.onGetCalendarLinkPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('RssSync')}
|
|
||||||
iconName={icons.RSS}
|
|
||||||
isSpinning={isRssSyncExecuting}
|
|
||||||
onPress={onRssSyncPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('SearchForMissing')}
|
|
||||||
iconName={icons.SEARCH}
|
|
||||||
isDisabled={!missingMovieIds.length}
|
|
||||||
isSpinning={isSearchingForMissing}
|
|
||||||
onPress={this.onSearchMissingPress}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.POSTER}
|
|
||||||
onPress={this.onOptionsPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FilterMenu
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
isDisabled={!hasMovie}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
|
||||||
filters={filters}
|
|
||||||
customFilters={customFilters}
|
|
||||||
filterModalConnectorComponent={CalendarFilterModal}
|
|
||||||
onFilterSelect={onFilterSelect}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<PageContentBody
|
|
||||||
className={styles.calendarPageBody}
|
|
||||||
innerClassName={styles.calendarInnerPageBody}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
movieIsFetching && !movieIsPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
movieError &&
|
|
||||||
<div className={styles.errorMessage}>
|
|
||||||
{getErrorMessage(movieError, 'Failed to load movies from API')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!movieError && movieIsPopulated && hasMovie &&
|
|
||||||
<Measure
|
|
||||||
whitelist={['width']}
|
|
||||||
onMeasure={this.onMeasure}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isMeasured ?
|
|
||||||
<CalendarConnector
|
|
||||||
useCurrentPage={useCurrentPage}
|
|
||||||
/> :
|
|
||||||
<div />
|
|
||||||
}
|
|
||||||
</Measure>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!movieError && movieIsPopulated && !hasMovie &&
|
|
||||||
<NoMovie totalItems={0} />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
hasMovie && !movieError &&
|
|
||||||
<LegendConnector />
|
|
||||||
}
|
|
||||||
</PageContentBody>
|
|
||||||
|
|
||||||
<CalendarLinkModal
|
|
||||||
isOpen={isCalendarLinkModalOpen}
|
|
||||||
onModalClose={this.onGetCalendarLinkModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CalendarOptionsModal
|
|
||||||
isOpen={isOptionsModalOpen}
|
|
||||||
onModalClose={this.onOptionsModalClose}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarPage.propTypes = {
|
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
hasMovie: PropTypes.bool.isRequired,
|
|
||||||
movieError: PropTypes.object,
|
|
||||||
movieIsFetching: PropTypes.bool.isRequired,
|
|
||||||
movieIsPopulated: PropTypes.bool.isRequired,
|
|
||||||
missingMovieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isRssSyncExecuting: PropTypes.bool.isRequired,
|
|
||||||
isSearchingForMissing: PropTypes.bool.isRequired,
|
|
||||||
useCurrentPage: PropTypes.bool.isRequired,
|
|
||||||
onSearchMissingPress: PropTypes.func.isRequired,
|
|
||||||
onDaysCountChange: PropTypes.func.isRequired,
|
|
||||||
onRssSyncPress: PropTypes.func.isRequired,
|
|
||||||
onFilterSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarPage;
|
|
||||||
224
frontend/src/Calendar/CalendarPage.tsx
Normal file
224
frontend/src/Calendar/CalendarPage.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||||
|
import { align, icons } from 'Helpers/Props';
|
||||||
|
import NoMovie from 'Movie/NoMovie';
|
||||||
|
import {
|
||||||
|
searchMissing,
|
||||||
|
setCalendarDaysCount,
|
||||||
|
setCalendarFilter,
|
||||||
|
} from 'Store/Actions/calendarActions';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
|
import createMovieCountSelector from 'Store/Selectors/createMovieCountSelector';
|
||||||
|
import { isCommandExecuting } from 'Utilities/Command';
|
||||||
|
import isBefore from 'Utilities/Date/isBefore';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import Calendar from './Calendar';
|
||||||
|
import CalendarFilterModal from './CalendarFilterModal';
|
||||||
|
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
||||||
|
import Legend from './Legend/Legend';
|
||||||
|
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
||||||
|
import styles from './CalendarPage.css';
|
||||||
|
|
||||||
|
const MINIMUM_DAY_WIDTH = 120;
|
||||||
|
|
||||||
|
function createMissingMovieIdsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.calendar.start,
|
||||||
|
(state: AppState) => state.calendar.end,
|
||||||
|
(state: AppState) => state.calendar.items,
|
||||||
|
(state: AppState) => state.queue.details.items,
|
||||||
|
(start, end, movies, queueDetails) => {
|
||||||
|
return movies.reduce<number[]>((acc, movie) => {
|
||||||
|
const { inCinemas } = movie;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!movie.movieFileId &&
|
||||||
|
inCinemas &&
|
||||||
|
moment(inCinemas).isAfter(start) &&
|
||||||
|
moment(inCinemas).isBefore(end) &&
|
||||||
|
isBefore(inCinemas) &&
|
||||||
|
!queueDetails.some(
|
||||||
|
(details) => !!details.movie && details.movie.id === movie.id
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
acc.push(movie.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createIsSearchingSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.calendar.searchMissingCommandId,
|
||||||
|
createCommandsSelector(),
|
||||||
|
(searchMissingCommandId, commands) => {
|
||||||
|
if (searchMissingCommandId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isCommandExecuting(
|
||||||
|
commands.find((command) => {
|
||||||
|
return command.id === searchMissingCommandId;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarPage() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { selectedFilterKey, filters } = useSelector(
|
||||||
|
(state: AppState) => state.calendar
|
||||||
|
);
|
||||||
|
const missingMovieIds = useSelector(createMissingMovieIdsSelector());
|
||||||
|
const isSearchingForMissing = useSelector(createIsSearchingSelector());
|
||||||
|
const isRssSyncExecuting = useSelector(
|
||||||
|
createCommandExecutingSelector(commandNames.RSS_SYNC)
|
||||||
|
);
|
||||||
|
const customFilters = useSelector(createCustomFiltersSelector('calendar'));
|
||||||
|
const hasMovies = !!useSelector(createMovieCountSelector());
|
||||||
|
|
||||||
|
const [pageContentRef, { width }] = useMeasure();
|
||||||
|
const [isCalendarLinkModalOpen, setIsCalendarLinkModalOpen] = useState(false);
|
||||||
|
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const isMeasured = width > 0;
|
||||||
|
const PageComponent = hasMovies ? Calendar : NoMovie;
|
||||||
|
|
||||||
|
const handleGetCalendarLinkPress = useCallback(() => {
|
||||||
|
setIsCalendarLinkModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleGetCalendarLinkModalClose = useCallback(() => {
|
||||||
|
setIsCalendarLinkModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOptionsPress = useCallback(() => {
|
||||||
|
setIsOptionsModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOptionsModalClose = useCallback(() => {
|
||||||
|
setIsOptionsModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRssSyncPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: commandNames.RSS_SYNC,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleSearchMissingPress = useCallback(() => {
|
||||||
|
dispatch(searchMissing({ movieIds: missingMovieIds }));
|
||||||
|
}, [missingMovieIds, dispatch]);
|
||||||
|
|
||||||
|
const handleFilterSelect = useCallback(
|
||||||
|
(key: string | number) => {
|
||||||
|
dispatch(setCalendarFilter({ selectedFilterKey: key }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (width === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayCount = Math.max(
|
||||||
|
3,
|
||||||
|
Math.min(7, Math.floor(width / MINIMUM_DAY_WIDTH))
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(setCalendarDaysCount({ dayCount }));
|
||||||
|
}, [width, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('Calendar')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('ICalLink')}
|
||||||
|
iconName={icons.CALENDAR}
|
||||||
|
onPress={handleGetCalendarLinkPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('RssSync')}
|
||||||
|
iconName={icons.RSS}
|
||||||
|
isSpinning={isRssSyncExecuting}
|
||||||
|
onPress={handleRssSyncPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('SearchForMissing')}
|
||||||
|
iconName={icons.SEARCH}
|
||||||
|
isDisabled={!missingMovieIds.length}
|
||||||
|
isSpinning={isSearchingForMissing}
|
||||||
|
onPress={handleSearchMissingPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.POSTER}
|
||||||
|
onPress={handleOptionsPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
isDisabled={!hasMovies}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={CalendarFilterModal}
|
||||||
|
onFilterSelect={handleFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBody
|
||||||
|
ref={pageContentRef}
|
||||||
|
className={styles.calendarPageBody}
|
||||||
|
innerClassName={styles.calendarInnerPageBody}
|
||||||
|
>
|
||||||
|
{isMeasured ? <PageComponent totalItems={0} /> : <div />}
|
||||||
|
{hasMovies && <Legend />}
|
||||||
|
</PageContentBody>
|
||||||
|
|
||||||
|
<CalendarLinkModal
|
||||||
|
isOpen={isCalendarLinkModalOpen}
|
||||||
|
onModalClose={handleGetCalendarLinkModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CalendarOptionsModal
|
||||||
|
isOpen={isOptionsModalOpen}
|
||||||
|
onModalClose={handleOptionsModalClose}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarPage;
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
|
||||||
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
|
||||||
import createMovieCountSelector from 'Store/Selectors/createMovieCountSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import { isCommandExecuting } from 'Utilities/Command';
|
|
||||||
import isBefore from 'Utilities/Date/isBefore';
|
|
||||||
import CalendarPage from './CalendarPage';
|
|
||||||
|
|
||||||
function createMissingMovieIdsSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar.start,
|
|
||||||
(state) => state.calendar.end,
|
|
||||||
(state) => state.calendar.items,
|
|
||||||
(state) => state.queue.details.items,
|
|
||||||
(start, end, movies, queueDetails) => {
|
|
||||||
return movies.reduce((acc, movie) => {
|
|
||||||
const inCinemas = movie.inCinemas;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!movie.hasFile &&
|
|
||||||
moment(inCinemas).isAfter(start) &&
|
|
||||||
moment(inCinemas).isBefore(end) &&
|
|
||||||
isBefore(movie.inCinemas) &&
|
|
||||||
!queueDetails.some((details) => details.movieId === movie.id)
|
|
||||||
) {
|
|
||||||
acc.push(movie.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createIsSearchingSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar.searchMissingCommandId,
|
|
||||||
createCommandsSelector(),
|
|
||||||
(searchMissingCommandId, commands) => {
|
|
||||||
if (searchMissingCommandId == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isCommandExecuting(commands.find((command) => {
|
|
||||||
return command.id === searchMissingCommandId;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar.selectedFilterKey,
|
|
||||||
(state) => state.calendar.filters,
|
|
||||||
createCustomFiltersSelector('calendar'),
|
|
||||||
createMovieCountSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
createMissingMovieIdsSelector(),
|
|
||||||
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
|
||||||
createIsSearchingSelector(),
|
|
||||||
(
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
customFilters,
|
|
||||||
movieCount,
|
|
||||||
uiSettings,
|
|
||||||
missingMovieIds,
|
|
||||||
isRssSyncExecuting,
|
|
||||||
isSearchingForMissing
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
customFilters,
|
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
|
||||||
hasMovie: !!movieCount.count,
|
|
||||||
movieError: movieCount.error,
|
|
||||||
movieIsFetching: movieCount.isFetching,
|
|
||||||
movieIsPopulated: movieCount.isPopulated,
|
|
||||||
missingMovieIds,
|
|
||||||
isRssSyncExecuting,
|
|
||||||
isSearchingForMissing
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onRssSyncPress() {
|
|
||||||
dispatch(executeCommand({
|
|
||||||
name: commandNames.RSS_SYNC
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
onSearchMissingPress(movieIds) {
|
|
||||||
dispatch(searchMissing({ movieIds }));
|
|
||||||
},
|
|
||||||
|
|
||||||
onDaysCountChange(dayCount) {
|
|
||||||
dispatch(setCalendarDaysCount({ dayCount }));
|
|
||||||
},
|
|
||||||
|
|
||||||
onFilterSelect(selectedFilterKey) {
|
|
||||||
dispatch(setCalendarFilter({ selectedFilterKey }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withCurrentPage(
|
|
||||||
connect(createMapStateToProps, createMapDispatchToProps)(CalendarPage)
|
|
||||||
);
|
|
||||||
@@ -1,23 +1,61 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
import * as calendarViews from 'Calendar/calendarViews';
|
import * as calendarViews from 'Calendar/calendarViews';
|
||||||
import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector';
|
import CalendarEvent from 'Calendar/Events/CalendarEvent';
|
||||||
import CalendarEvent from 'typings/CalendarEvent';
|
import { CalendarEvent as CalendarEventModel } from 'typings/Calendar';
|
||||||
import styles from './CalendarDay.css';
|
import styles from './CalendarDay.css';
|
||||||
|
|
||||||
|
function sort(items: CalendarEventModel[]) {
|
||||||
|
return items.sort((a, b) => {
|
||||||
|
const aDate = moment(a.inCinemas).unix();
|
||||||
|
const bDate = moment(b.inCinemas).unix();
|
||||||
|
|
||||||
|
return aDate - bDate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCalendarEventsConnector(date: string) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.calendar.items,
|
||||||
|
(state: AppState) => state.calendar.options,
|
||||||
|
(items, options) => {
|
||||||
|
const { showCinemaRelease, showDigitalRelease, showPhysicalRelease } =
|
||||||
|
options;
|
||||||
|
const momentDate = moment(date);
|
||||||
|
|
||||||
|
const filtered = items.filter(
|
||||||
|
({ inCinemas, digitalRelease, physicalRelease }) => {
|
||||||
|
return (
|
||||||
|
(showCinemaRelease &&
|
||||||
|
inCinemas &&
|
||||||
|
momentDate.isSame(moment(inCinemas), 'day')) ||
|
||||||
|
(showDigitalRelease &&
|
||||||
|
digitalRelease &&
|
||||||
|
momentDate.isSame(moment(digitalRelease), 'day')) ||
|
||||||
|
(showPhysicalRelease &&
|
||||||
|
physicalRelease &&
|
||||||
|
momentDate.isSame(moment(physicalRelease), 'day'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return sort(filtered);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface CalendarDayProps {
|
interface CalendarDayProps {
|
||||||
date: string;
|
date: string;
|
||||||
time: string;
|
|
||||||
isTodaysDate: boolean;
|
isTodaysDate: boolean;
|
||||||
events: CalendarEvent[];
|
|
||||||
view: string;
|
|
||||||
onEventModalOpenToggle(...args: unknown[]): unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function CalendarDay(props: CalendarDayProps) {
|
function CalendarDay({ date, isTodaysDate }: CalendarDayProps) {
|
||||||
const { date, time, isTodaysDate, events, view, onEventModalOpenToggle } =
|
const { time, view } = useSelector((state: AppState) => state.calendar);
|
||||||
props;
|
const events = useSelector(createCalendarEventsConnector(date));
|
||||||
|
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -50,13 +88,7 @@ function CalendarDay(props: CalendarDayProps) {
|
|||||||
<div>
|
<div>
|
||||||
{events.map((event) => {
|
{events.map((event) => {
|
||||||
return (
|
return (
|
||||||
<CalendarEventConnector
|
<CalendarEvent key={event.id} {...event} date={date as string} />
|
||||||
key={event.id}
|
|
||||||
{...event}
|
|
||||||
movieId={event.id}
|
|
||||||
date={date as string}
|
|
||||||
onEventModalOpenToggle={onEventModalOpenToggle}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import CalendarDay from './CalendarDay';
|
|
||||||
|
|
||||||
function sort(items) {
|
|
||||||
return _.sortBy(items, (item) => {
|
|
||||||
if (item.isGroup) {
|
|
||||||
return moment(item.events[0].inCinemas).unix();
|
|
||||||
}
|
|
||||||
|
|
||||||
return moment(item.inCinemas).unix();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCalendarEventsConnector() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { date }) => date,
|
|
||||||
(state) => state.calendar.items,
|
|
||||||
(date, items) => {
|
|
||||||
const filtered = _.filter(items, (item) => {
|
|
||||||
return (item.inCinemas && moment(date).isSame(moment(item.inCinemas), 'day')) ||
|
|
||||||
(item.physicalRelease && moment(date).isSame(moment(item.physicalRelease), 'day')) ||
|
|
||||||
(item.digitalRelease && moment(date).isSame(moment(item.digitalRelease), 'day'));
|
|
||||||
});
|
|
||||||
|
|
||||||
return sort(filtered);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar,
|
|
||||||
createCalendarEventsConnector(),
|
|
||||||
(calendar, events) => {
|
|
||||||
return {
|
|
||||||
time: calendar.time,
|
|
||||||
view: calendar.view,
|
|
||||||
events
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class CalendarDayConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<CalendarDay
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarDayConnector.propTypes = {
|
|
||||||
date: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(CalendarDayConnector);
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import * as calendarViews from 'Calendar/calendarViews';
|
|
||||||
import isToday from 'Utilities/Date/isToday';
|
|
||||||
import CalendarDayConnector from './CalendarDayConnector';
|
|
||||||
import styles from './CalendarDays.css';
|
|
||||||
|
|
||||||
class CalendarDays extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._touchStart = null;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
todaysDate: moment().startOf('day').toISOString(),
|
|
||||||
isEventModalOpen: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this.updateTimeoutId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const view = this.props.view;
|
|
||||||
|
|
||||||
if (view === calendarViews.MONTH) {
|
|
||||||
this.scheduleUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('touchstart', this.onTouchStart);
|
|
||||||
window.addEventListener('touchend', this.onTouchEnd);
|
|
||||||
window.addEventListener('touchcancel', this.onTouchCancel);
|
|
||||||
window.addEventListener('touchmove', this.onTouchMove);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.clearUpdateTimeout();
|
|
||||||
|
|
||||||
window.removeEventListener('touchstart', this.onTouchStart);
|
|
||||||
window.removeEventListener('touchend', this.onTouchEnd);
|
|
||||||
window.removeEventListener('touchcancel', this.onTouchCancel);
|
|
||||||
window.removeEventListener('touchmove', this.onTouchMove);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
scheduleUpdate = () => {
|
|
||||||
this.clearUpdateTimeout();
|
|
||||||
const todaysDate = moment().startOf('day');
|
|
||||||
const diff = moment().diff(todaysDate.clone().add(1, 'day'));
|
|
||||||
|
|
||||||
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;
|
|
||||||
const touchStart = touches[0].pageX;
|
|
||||||
|
|
||||||
if (touches.length !== 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
touchStart < 50 ||
|
|
||||||
this.props.isSidebarVisible ||
|
|
||||||
this.state.isEventModalOpen
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._touchStart = touchStart;
|
|
||||||
};
|
|
||||||
|
|
||||||
onTouchEnd = (event) => {
|
|
||||||
const touches = event.changedTouches;
|
|
||||||
const currentTouch = touches[0].pageX;
|
|
||||||
|
|
||||||
if (!this._touchStart) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTouch > this._touchStart && currentTouch - this._touchStart > 100) {
|
|
||||||
this.props.onNavigatePrevious();
|
|
||||||
} else if (currentTouch < this._touchStart && this._touchStart - currentTouch > 100) {
|
|
||||||
this.props.onNavigateNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._touchStart = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
onTouchCancel = (event) => {
|
|
||||||
this._touchStart = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
onTouchMove = (event) => {
|
|
||||||
if (!this._touchStart) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
dates,
|
|
||||||
view
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(
|
|
||||||
styles.days,
|
|
||||||
styles[view]
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
dates.map((date) => {
|
|
||||||
return (
|
|
||||||
<CalendarDayConnector
|
|
||||||
key={date}
|
|
||||||
date={date}
|
|
||||||
isTodaysDate={isToday(date)}
|
|
||||||
onEventModalOpenToggle={this.onEventModalOpenToggle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarDays.propTypes = {
|
|
||||||
dates: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
view: PropTypes.string.isRequired,
|
|
||||||
isSidebarVisible: PropTypes.bool.isRequired,
|
|
||||||
onNavigatePrevious: PropTypes.func.isRequired,
|
|
||||||
onNavigateNext: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarDays;
|
|
||||||
129
frontend/src/Calendar/Day/CalendarDays.tsx
Normal file
129
frontend/src/Calendar/Day/CalendarDays.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import moment from 'moment';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import * as calendarViews from 'Calendar/calendarViews';
|
||||||
|
import {
|
||||||
|
gotoCalendarNextRange,
|
||||||
|
gotoCalendarPreviousRange,
|
||||||
|
} from 'Store/Actions/calendarActions';
|
||||||
|
import CalendarDay from './CalendarDay';
|
||||||
|
import styles from './CalendarDays.css';
|
||||||
|
|
||||||
|
function CalendarDays() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { dates, view } = useSelector((state: AppState) => state.calendar);
|
||||||
|
const isSidebarVisible = useSelector(
|
||||||
|
(state: AppState) => state.app.isSidebarVisible
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const touchStart = useRef<number | null>(null);
|
||||||
|
const [todaysDate, setTodaysDate] = useState(
|
||||||
|
moment().startOf('day').toISOString()
|
||||||
|
);
|
||||||
|
|
||||||
|
const scheduleUpdate = useCallback(() => {
|
||||||
|
clearTimeout(updateTimeout.current);
|
||||||
|
|
||||||
|
const todaysDate = moment().startOf('day');
|
||||||
|
const diff = moment().diff(todaysDate.clone().add(1, 'day'));
|
||||||
|
|
||||||
|
setTodaysDate(todaysDate.toISOString());
|
||||||
|
|
||||||
|
updateTimeout.current = setTimeout(scheduleUpdate, diff);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTouchStart = useCallback(
|
||||||
|
(event: TouchEvent) => {
|
||||||
|
const touches = event.touches;
|
||||||
|
const currentTouch = touches[0].pageX;
|
||||||
|
|
||||||
|
if (touches.length !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTouch < 50 || isSidebarVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
touchStart.current = currentTouch;
|
||||||
|
},
|
||||||
|
[isSidebarVisible]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTouchEnd = useCallback(
|
||||||
|
(event: TouchEvent) => {
|
||||||
|
const touches = event.changedTouches;
|
||||||
|
const currentTouch = touches[0].pageX;
|
||||||
|
|
||||||
|
if (!touchStart.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentTouch > touchStart.current &&
|
||||||
|
currentTouch - touchStart.current > 100
|
||||||
|
) {
|
||||||
|
dispatch(gotoCalendarPreviousRange());
|
||||||
|
} else if (
|
||||||
|
currentTouch < touchStart.current &&
|
||||||
|
touchStart.current - currentTouch > 100
|
||||||
|
) {
|
||||||
|
dispatch(gotoCalendarNextRange());
|
||||||
|
}
|
||||||
|
|
||||||
|
touchStart.current = null;
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTouchCancel = useCallback(() => {
|
||||||
|
touchStart.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTouchMove = useCallback(() => {
|
||||||
|
if (!touchStart.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (view === calendarViews.MONTH) {
|
||||||
|
scheduleUpdate();
|
||||||
|
}
|
||||||
|
}, [view, scheduleUpdate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('touchstart', handleTouchStart);
|
||||||
|
window.addEventListener('touchend', handleTouchEnd);
|
||||||
|
window.addEventListener('touchcancel', handleTouchCancel);
|
||||||
|
window.addEventListener('touchmove', handleTouchMove);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('touchstart', handleTouchStart);
|
||||||
|
window.removeEventListener('touchend', handleTouchEnd);
|
||||||
|
window.removeEventListener('touchcancel', handleTouchCancel);
|
||||||
|
window.removeEventListener('touchmove', handleTouchMove);
|
||||||
|
};
|
||||||
|
}, [handleTouchStart, handleTouchEnd, handleTouchCancel, handleTouchMove]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.days, styles[view as keyof typeof styles])}
|
||||||
|
>
|
||||||
|
{dates.map((date) => {
|
||||||
|
return (
|
||||||
|
<CalendarDay
|
||||||
|
key={date}
|
||||||
|
date={date}
|
||||||
|
isTodaysDate={date === todaysDate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarDays;
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { gotoCalendarNextRange, gotoCalendarPreviousRange } from 'Store/Actions/calendarActions';
|
|
||||||
import CalendarDays from './CalendarDays';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar,
|
|
||||||
(state) => state.app.isSidebarVisible,
|
|
||||||
(calendar, isSidebarVisible) => {
|
|
||||||
return {
|
|
||||||
dates: calendar.dates,
|
|
||||||
view: calendar.view,
|
|
||||||
isSidebarVisible
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
onNavigatePrevious: gotoCalendarPreviousRange,
|
|
||||||
onNavigateNext: gotoCalendarNextRange
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarDays);
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import * as calendarViews from 'Calendar/calendarViews';
|
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
|
||||||
import styles from './DayOfWeek.css';
|
|
||||||
|
|
||||||
class DayOfWeek extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
date,
|
|
||||||
view,
|
|
||||||
isTodaysDate,
|
|
||||||
calendarWeekColumnHeader,
|
|
||||||
shortDateFormat,
|
|
||||||
showRelativeDates
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const highlightToday = view !== calendarViews.MONTH && isTodaysDate;
|
|
||||||
const momentDate = moment(date);
|
|
||||||
let formatedDate = momentDate.format('dddd');
|
|
||||||
|
|
||||||
if (view === calendarViews.WEEK) {
|
|
||||||
formatedDate = momentDate.format(calendarWeekColumnHeader);
|
|
||||||
} else if (view === calendarViews.FORECAST) {
|
|
||||||
formatedDate = getRelativeDate({ date, shortDateFormat, showRelativeDates });
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(
|
|
||||||
styles.dayOfWeek,
|
|
||||||
view === calendarViews.DAY && styles.isSingleDay,
|
|
||||||
highlightToday && styles.isToday
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formatedDate}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DayOfWeek.propTypes = {
|
|
||||||
date: PropTypes.string.isRequired,
|
|
||||||
view: PropTypes.string.isRequired,
|
|
||||||
isTodaysDate: PropTypes.bool.isRequired,
|
|
||||||
calendarWeekColumnHeader: PropTypes.string.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DayOfWeek;
|
|
||||||
54
frontend/src/Calendar/Day/DayOfWeek.tsx
Normal file
54
frontend/src/Calendar/Day/DayOfWeek.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import moment from 'moment';
|
||||||
|
import React from 'react';
|
||||||
|
import * as calendarViews from 'Calendar/calendarViews';
|
||||||
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
|
import styles from './DayOfWeek.css';
|
||||||
|
|
||||||
|
interface DayOfWeekProps {
|
||||||
|
date: string;
|
||||||
|
view: string;
|
||||||
|
isTodaysDate: boolean;
|
||||||
|
calendarWeekColumnHeader: string;
|
||||||
|
shortDateFormat: string;
|
||||||
|
showRelativeDates: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DayOfWeek(props: DayOfWeekProps) {
|
||||||
|
const {
|
||||||
|
date,
|
||||||
|
view,
|
||||||
|
isTodaysDate,
|
||||||
|
calendarWeekColumnHeader,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const highlightToday = view !== calendarViews.MONTH && isTodaysDate;
|
||||||
|
const momentDate = moment(date);
|
||||||
|
let formatedDate = momentDate.format('dddd');
|
||||||
|
|
||||||
|
if (view === calendarViews.WEEK) {
|
||||||
|
formatedDate = momentDate.format(calendarWeekColumnHeader);
|
||||||
|
} else if (view === calendarViews.FORECAST) {
|
||||||
|
formatedDate = getRelativeDate({
|
||||||
|
date,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.dayOfWeek,
|
||||||
|
view === calendarViews.DAY && styles.isSingleDay,
|
||||||
|
highlightToday && styles.isToday
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatedDate}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DayOfWeek;
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import * as calendarViews from 'Calendar/calendarViews';
|
|
||||||
import DayOfWeek from './DayOfWeek';
|
|
||||||
import styles from './DaysOfWeek.css';
|
|
||||||
|
|
||||||
class DaysOfWeek extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
todaysDate: moment().startOf('day').toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
this.updateTimeoutId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const view = this.props.view;
|
|
||||||
|
|
||||||
if (view !== calendarViews.AGENDA || view !== calendarViews.MONTH) {
|
|
||||||
this.scheduleUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.clearUpdateTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
scheduleUpdate = () => {
|
|
||||||
this.clearUpdateTimeout();
|
|
||||||
const todaysDate = moment().startOf('day');
|
|
||||||
const diff = todaysDate.clone().add(1, 'day').diff(moment());
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
todaysDate: todaysDate.toISOString()
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff);
|
|
||||||
};
|
|
||||||
|
|
||||||
clearUpdateTimeout = () => {
|
|
||||||
if (this.updateTimeoutId) {
|
|
||||||
clearTimeout(this.updateTimeoutId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
dates,
|
|
||||||
view,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (view === calendarViews.AGENDA) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.daysOfWeek}>
|
|
||||||
{
|
|
||||||
dates.map((date) => {
|
|
||||||
return (
|
|
||||||
<DayOfWeek
|
|
||||||
key={date}
|
|
||||||
date={date}
|
|
||||||
view={view}
|
|
||||||
isTodaysDate={date === this.state.todaysDate}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DaysOfWeek.propTypes = {
|
|
||||||
dates: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
view: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DaysOfWeek;
|
|
||||||
60
frontend/src/Calendar/Day/DaysOfWeek.tsx
Normal file
60
frontend/src/Calendar/Day/DaysOfWeek.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import * as calendarViews from 'Calendar/calendarViews';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import DayOfWeek from './DayOfWeek';
|
||||||
|
import styles from './DaysOfWeek.css';
|
||||||
|
|
||||||
|
function DaysOfWeek() {
|
||||||
|
const { dates, view } = useSelector((state: AppState) => state.calendar);
|
||||||
|
const { calendarWeekColumnHeader, shortDateFormat, showRelativeDates } =
|
||||||
|
useSelector(createUISettingsSelector());
|
||||||
|
|
||||||
|
const updateTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const [todaysDate, setTodaysDate] = useState(
|
||||||
|
moment().startOf('day').toISOString()
|
||||||
|
);
|
||||||
|
|
||||||
|
const scheduleUpdate = useCallback(() => {
|
||||||
|
clearTimeout(updateTimeout.current);
|
||||||
|
|
||||||
|
const todaysDate = moment().startOf('day');
|
||||||
|
const diff = moment().diff(todaysDate.clone().add(1, 'day'));
|
||||||
|
|
||||||
|
setTodaysDate(todaysDate.toISOString());
|
||||||
|
|
||||||
|
updateTimeout.current = setTimeout(scheduleUpdate, diff);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (view !== calendarViews.AGENDA && view !== calendarViews.MONTH) {
|
||||||
|
scheduleUpdate();
|
||||||
|
}
|
||||||
|
}, [view, scheduleUpdate]);
|
||||||
|
|
||||||
|
if (view === calendarViews.AGENDA) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.daysOfWeek}>
|
||||||
|
{dates.map((date) => {
|
||||||
|
return (
|
||||||
|
<DayOfWeek
|
||||||
|
key={date}
|
||||||
|
date={date}
|
||||||
|
view={view}
|
||||||
|
isTodaysDate={date === todaysDate}
|
||||||
|
calendarWeekColumnHeader={calendarWeekColumnHeader}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
showRelativeDates={showRelativeDates}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DaysOfWeek;
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import DaysOfWeek from './DaysOfWeek';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar,
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(calendar, UiSettings) => {
|
|
||||||
return {
|
|
||||||
dates: calendar.dates.slice(0, 7),
|
|
||||||
view: calendar.view,
|
|
||||||
calendarWeekColumnHeader: UiSettings.calendarWeekColumnHeader,
|
|
||||||
shortDateFormat: UiSettings.shortDateFormat,
|
|
||||||
showRelativeDates: UiSettings.showRelativeDates
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(DaysOfWeek);
|
|
||||||
@@ -34,7 +34,8 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.movieTitle,
|
.movieTitle,
|
||||||
.genres {
|
.genres,
|
||||||
|
.eventType {
|
||||||
@add-mixin truncate;
|
@add-mixin truncate;
|
||||||
flex: 1 0 1px;
|
flex: 1 0 1px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ interface CssExports {
|
|||||||
'continuing': string;
|
'continuing': string;
|
||||||
'downloaded': string;
|
'downloaded': string;
|
||||||
'event': string;
|
'event': string;
|
||||||
|
'eventType': string;
|
||||||
'genres': string;
|
'genres': string;
|
||||||
'info': string;
|
'info': string;
|
||||||
'missingMonitored': string;
|
'missingMonitored': string;
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
|
|
||||||
import styles from './CalendarEvent.css';
|
|
||||||
|
|
||||||
class CalendarEvent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
movieFile,
|
|
||||||
isAvailable,
|
|
||||||
inCinemas,
|
|
||||||
physicalRelease,
|
|
||||||
digitalRelease,
|
|
||||||
title,
|
|
||||||
titleSlug,
|
|
||||||
genres,
|
|
||||||
date,
|
|
||||||
monitored,
|
|
||||||
certification,
|
|
||||||
hasFile,
|
|
||||||
grabbed,
|
|
||||||
queueItem,
|
|
||||||
showMovieInformation,
|
|
||||||
showCutoffUnmetIcon,
|
|
||||||
fullColorEvents,
|
|
||||||
colorImpairedMode
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const isDownloading = !!(queueItem || grabbed);
|
|
||||||
const isMonitored = monitored;
|
|
||||||
const statusStyle = getStatusStyle(hasFile, isDownloading, isMonitored, isAvailable);
|
|
||||||
const joinedGenres = genres.slice(0, 2).join(', ');
|
|
||||||
const link = `/movie/${titleSlug}`;
|
|
||||||
const eventType = [];
|
|
||||||
|
|
||||||
if (inCinemas && moment(date).isSame(moment(inCinemas), 'day')) {
|
|
||||||
eventType.push('Cinemas');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (physicalRelease && moment(date).isSame(moment(physicalRelease), 'day')) {
|
|
||||||
eventType.push('Physical');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (digitalRelease && moment(date).isSame(moment(digitalRelease), 'day')) {
|
|
||||||
eventType.push('Digital');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.event,
|
|
||||||
styles[statusStyle],
|
|
||||||
colorImpairedMode && 'colorImpaired',
|
|
||||||
fullColorEvents && 'fullColor'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
className={styles.underlay}
|
|
||||||
to={link}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.overlay} >
|
|
||||||
<div className={styles.info}>
|
|
||||||
<div className={styles.movieTitle}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.statusContainer,
|
|
||||||
fullColorEvents && 'fullColor'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
queueItem ?
|
|
||||||
<span className={styles.statusIcon}>
|
|
||||||
<CalendarEventQueueDetails
|
|
||||||
{...queueItem}
|
|
||||||
fullColorEvents={fullColorEvents}
|
|
||||||
/>
|
|
||||||
</span> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!queueItem && grabbed ?
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.DOWNLOADING}
|
|
||||||
title={translate('MovieIsDownloading')}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showCutoffUnmetIcon &&
|
|
||||||
!!movieFile &&
|
|
||||||
movieFile.qualityCutoffNotMet ?
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.MOVIE_FILE}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
title={translate('QualityCutoffNotMet')}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
showMovieInformation ?
|
|
||||||
<div className={styles.movieInfo}>
|
|
||||||
<div className={styles.genres}>
|
|
||||||
{joinedGenres}
|
|
||||||
</div>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showMovieInformation ?
|
|
||||||
<div className={styles.movieInfo}>
|
|
||||||
<div className={styles.genres}>
|
|
||||||
{eventType.join(', ')}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{certification}
|
|
||||||
</div>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarEvent.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
movieFile: PropTypes.object,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
titleSlug: PropTypes.string.isRequired,
|
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
|
||||||
inCinemas: PropTypes.string,
|
|
||||||
physicalRelease: PropTypes.string,
|
|
||||||
digitalRelease: PropTypes.string,
|
|
||||||
date: PropTypes.string.isRequired,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
certification: PropTypes.string,
|
|
||||||
hasFile: PropTypes.bool.isRequired,
|
|
||||||
grabbed: PropTypes.bool,
|
|
||||||
queueItem: PropTypes.object,
|
|
||||||
// These props come from the connector, not marked as required to appease TS for now.
|
|
||||||
showMovieInformation: PropTypes.bool,
|
|
||||||
showCutoffUnmetIcon: PropTypes.bool,
|
|
||||||
fullColorEvents: PropTypes.bool,
|
|
||||||
timeFormat: PropTypes.string,
|
|
||||||
colorImpairedMode: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
CalendarEvent.defaultProps = {
|
|
||||||
genres: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarEvent;
|
|
||||||
180
frontend/src/Calendar/Events/CalendarEvent.tsx
Normal file
180
frontend/src/Calendar/Events/CalendarEvent.tsx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import moment from 'moment';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import useMovieFile from 'MovieFile/useMovieFile';
|
||||||
|
import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
|
||||||
|
import styles from './CalendarEvent.css';
|
||||||
|
|
||||||
|
interface CalendarEventProps {
|
||||||
|
id: number;
|
||||||
|
movieFileId?: number;
|
||||||
|
title: string;
|
||||||
|
titleSlug: string;
|
||||||
|
genres: string[];
|
||||||
|
certification?: string;
|
||||||
|
date: string;
|
||||||
|
inCinemas?: string;
|
||||||
|
digitalRelease?: string;
|
||||||
|
physicalRelease?: string;
|
||||||
|
isAvailable: boolean;
|
||||||
|
monitored: boolean;
|
||||||
|
hasFile: boolean;
|
||||||
|
grabbed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarEvent({
|
||||||
|
id,
|
||||||
|
movieFileId,
|
||||||
|
title,
|
||||||
|
titleSlug,
|
||||||
|
genres = [],
|
||||||
|
certification,
|
||||||
|
date,
|
||||||
|
inCinemas,
|
||||||
|
digitalRelease,
|
||||||
|
physicalRelease,
|
||||||
|
isAvailable,
|
||||||
|
monitored: isMonitored,
|
||||||
|
hasFile,
|
||||||
|
grabbed,
|
||||||
|
}: CalendarEventProps) {
|
||||||
|
const movieFile = useMovieFile(movieFileId);
|
||||||
|
const queueItem = useSelector(createQueueItemSelectorForHook(id));
|
||||||
|
|
||||||
|
const { enableColorImpairedMode } = useSelector(createUISettingsSelector());
|
||||||
|
|
||||||
|
const {
|
||||||
|
showMovieInformation,
|
||||||
|
showCinemaRelease,
|
||||||
|
showDigitalRelease,
|
||||||
|
showPhysicalRelease,
|
||||||
|
showCutoffUnmetIcon,
|
||||||
|
fullColorEvents,
|
||||||
|
} = useSelector((state: AppState) => state.calendar.options);
|
||||||
|
|
||||||
|
const isDownloading = !!(queueItem || grabbed);
|
||||||
|
const statusStyle = getStatusStyle(
|
||||||
|
hasFile,
|
||||||
|
isDownloading,
|
||||||
|
isMonitored,
|
||||||
|
isAvailable
|
||||||
|
);
|
||||||
|
const joinedGenres = genres.slice(0, 2).join(', ');
|
||||||
|
const link = `/movie/${titleSlug}`;
|
||||||
|
|
||||||
|
const eventTypes = useMemo(() => {
|
||||||
|
const momentDate = moment(date);
|
||||||
|
|
||||||
|
const types = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
showCinemaRelease &&
|
||||||
|
inCinemas &&
|
||||||
|
momentDate.isSame(moment(inCinemas), 'day')
|
||||||
|
) {
|
||||||
|
types.push('Cinemas');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
showDigitalRelease &&
|
||||||
|
digitalRelease &&
|
||||||
|
momentDate.isSame(moment(digitalRelease), 'day')
|
||||||
|
) {
|
||||||
|
types.push('Digital');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
showPhysicalRelease &&
|
||||||
|
physicalRelease &&
|
||||||
|
momentDate.isSame(moment(physicalRelease), 'day')
|
||||||
|
) {
|
||||||
|
types.push('Physical');
|
||||||
|
}
|
||||||
|
|
||||||
|
return types;
|
||||||
|
}, [
|
||||||
|
date,
|
||||||
|
showCinemaRelease,
|
||||||
|
showDigitalRelease,
|
||||||
|
showPhysicalRelease,
|
||||||
|
inCinemas,
|
||||||
|
digitalRelease,
|
||||||
|
physicalRelease,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.event,
|
||||||
|
styles[statusStyle],
|
||||||
|
enableColorImpairedMode && 'colorImpaired',
|
||||||
|
fullColorEvents && 'fullColor'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link className={styles.underlay} to={link} />
|
||||||
|
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
<div className={styles.info}>
|
||||||
|
<div className={styles.movieTitle}>{title}</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.statusContainer,
|
||||||
|
fullColorEvents && 'fullColor'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{queueItem ? (
|
||||||
|
<span className={styles.statusIcon}>
|
||||||
|
<CalendarEventQueueDetails {...queueItem} />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!queueItem && grabbed ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.DOWNLOADING}
|
||||||
|
title={translate('MovieIsDownloading')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showCutoffUnmetIcon &&
|
||||||
|
!!movieFile &&
|
||||||
|
movieFile.qualityCutoffNotMet ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.MOVIE_FILE}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
title={translate('QualityCutoffNotMet')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showMovieInformation ? (
|
||||||
|
<>
|
||||||
|
<div className={styles.movieInfo}>
|
||||||
|
<div className={styles.genres}>{joinedGenres}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.movieInfo}>
|
||||||
|
<div className={styles.eventType}>{eventTypes.join(', ')}</div>
|
||||||
|
|
||||||
|
<div>{certification}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarEvent;
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
|
||||||
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import CalendarEvent from './CalendarEvent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar.options,
|
|
||||||
createMovieSelector(),
|
|
||||||
createQueueItemSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(calendarOptions, movie, queueItem, uiSettings) => {
|
|
||||||
return {
|
|
||||||
movie,
|
|
||||||
queueItem,
|
|
||||||
...calendarOptions,
|
|
||||||
timeFormat: uiSettings.timeFormat,
|
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(CalendarEvent);
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import QueueDetails from 'Activity/Queue/QueueDetails';
|
|
||||||
import CircularProgressBar from 'Components/CircularProgressBar';
|
|
||||||
|
|
||||||
function CalendarEventQueueDetails(props) {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
size,
|
|
||||||
sizeleft,
|
|
||||||
estimatedCompletionTime,
|
|
||||||
status,
|
|
||||||
trackedDownloadState,
|
|
||||||
trackedDownloadStatus,
|
|
||||||
statusMessages,
|
|
||||||
errorMessage
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QueueDetails
|
|
||||||
title={title}
|
|
||||||
size={size}
|
|
||||||
sizeleft={sizeleft}
|
|
||||||
estimatedCompletionTime={estimatedCompletionTime}
|
|
||||||
status={status}
|
|
||||||
trackedDownloadState={trackedDownloadState}
|
|
||||||
trackedDownloadStatus={trackedDownloadStatus}
|
|
||||||
statusMessages={statusMessages}
|
|
||||||
errorMessage={errorMessage}
|
|
||||||
progressBar={
|
|
||||||
<CircularProgressBar
|
|
||||||
progress={progress}
|
|
||||||
size={20}
|
|
||||||
strokeWidth={2}
|
|
||||||
strokeColor={'#7a43b6'}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarEventQueueDetails.propTypes = {
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
sizeleft: PropTypes.number.isRequired,
|
|
||||||
estimatedCompletionTime: PropTypes.string,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadState: PropTypes.string.isRequired,
|
|
||||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
errorMessage: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarEventQueueDetails;
|
|
||||||
58
frontend/src/Calendar/Events/CalendarEventQueueDetails.tsx
Normal file
58
frontend/src/Calendar/Events/CalendarEventQueueDetails.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import QueueDetails from 'Activity/Queue/QueueDetails';
|
||||||
|
import CircularProgressBar from 'Components/CircularProgressBar';
|
||||||
|
import {
|
||||||
|
QueueTrackedDownloadState,
|
||||||
|
QueueTrackedDownloadStatus,
|
||||||
|
StatusMessage,
|
||||||
|
} from 'typings/Queue';
|
||||||
|
|
||||||
|
interface CalendarEventQueueDetailsProps {
|
||||||
|
title: string;
|
||||||
|
size: number;
|
||||||
|
sizeleft: number;
|
||||||
|
estimatedCompletionTime?: string;
|
||||||
|
status: string;
|
||||||
|
trackedDownloadState: QueueTrackedDownloadState;
|
||||||
|
trackedDownloadStatus: QueueTrackedDownloadStatus;
|
||||||
|
statusMessages?: StatusMessage[];
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarEventQueueDetails({
|
||||||
|
title,
|
||||||
|
size,
|
||||||
|
sizeleft,
|
||||||
|
estimatedCompletionTime,
|
||||||
|
status,
|
||||||
|
trackedDownloadState,
|
||||||
|
trackedDownloadStatus,
|
||||||
|
statusMessages,
|
||||||
|
errorMessage,
|
||||||
|
}: CalendarEventQueueDetailsProps) {
|
||||||
|
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueueDetails
|
||||||
|
title={title}
|
||||||
|
size={size}
|
||||||
|
sizeleft={sizeleft}
|
||||||
|
estimatedCompletionTime={estimatedCompletionTime}
|
||||||
|
status={status}
|
||||||
|
trackedDownloadState={trackedDownloadState}
|
||||||
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
|
statusMessages={statusMessages}
|
||||||
|
errorMessage={errorMessage}
|
||||||
|
progressBar={
|
||||||
|
<CircularProgressBar
|
||||||
|
progress={progress}
|
||||||
|
size={20}
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeColor="#7a43b6"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarEventQueueDetails;
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import * as calendarViews from 'Calendar/calendarViews';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import Menu from 'Components/Menu/Menu';
|
|
||||||
import MenuButton from 'Components/Menu/MenuButton';
|
|
||||||
import MenuContent from 'Components/Menu/MenuContent';
|
|
||||||
import ViewMenuItem from 'Components/Menu/ViewMenuItem';
|
|
||||||
import { align, icons } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import CalendarHeaderViewButton from './CalendarHeaderViewButton';
|
|
||||||
import styles from './CalendarHeader.css';
|
|
||||||
|
|
||||||
function getTitle(time, start, end, view, longDateFormat) {
|
|
||||||
const timeMoment = moment(time);
|
|
||||||
const startMoment = moment(start);
|
|
||||||
const endMoment = moment(end);
|
|
||||||
|
|
||||||
if (view === 'day') {
|
|
||||||
return timeMoment.format(longDateFormat);
|
|
||||||
} else if (view === 'month') {
|
|
||||||
return timeMoment.format('MMMM YYYY');
|
|
||||||
} else if (view === 'agenda') {
|
|
||||||
return `Agenda: ${startMoment.format('MMM D')} - ${endMoment.format('MMM D')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
let startFormat = 'MMM D YYYY';
|
|
||||||
let endFormat = 'MMM D YYYY';
|
|
||||||
|
|
||||||
if (startMoment.isSame(endMoment, 'month')) {
|
|
||||||
startFormat = 'MMM D';
|
|
||||||
endFormat = 'D YYYY';
|
|
||||||
} else if (startMoment.isSame(endMoment, 'year')) {
|
|
||||||
startFormat = 'MMM D';
|
|
||||||
endFormat = 'MMM D YYYY';
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${startMoment.format(startFormat)} \u2014 ${endMoment.format(endFormat)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Convert to a stateful Component so we can track view internally when changed
|
|
||||||
|
|
||||||
class CalendarHeader extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
view: props.view
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const view = this.props.view;
|
|
||||||
|
|
||||||
if (prevProps.view !== view) {
|
|
||||||
this.setState({ view });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onViewChange = (view) => {
|
|
||||||
this.setState({ view }, () => {
|
|
||||||
this.props.onViewChange(view);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
time,
|
|
||||||
start,
|
|
||||||
end,
|
|
||||||
longDateFormat,
|
|
||||||
isSmallScreen,
|
|
||||||
collapseViewButtons,
|
|
||||||
onTodayPress,
|
|
||||||
onPreviousPress,
|
|
||||||
onNextPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const view = this.state.view;
|
|
||||||
|
|
||||||
const title = getTitle(time, start, end, view, longDateFormat);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
isSmallScreen &&
|
|
||||||
<div className={styles.titleMobile}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className={styles.header}>
|
|
||||||
<div className={styles.navigationButtons}>
|
|
||||||
<Button
|
|
||||||
buttonGroupPosition={align.LEFT}
|
|
||||||
isDisabled={view === calendarViews.AGENDA}
|
|
||||||
onPress={onPreviousPress}
|
|
||||||
>
|
|
||||||
<Icon name={icons.PAGE_PREVIOUS} />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
buttonGroupPosition={align.RIGHT}
|
|
||||||
isDisabled={view === calendarViews.AGENDA}
|
|
||||||
onPress={onNextPress}
|
|
||||||
>
|
|
||||||
<Icon name={icons.PAGE_NEXT} />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className={styles.todayButton}
|
|
||||||
isDisabled={view === calendarViews.AGENDA}
|
|
||||||
onPress={onTodayPress}
|
|
||||||
>
|
|
||||||
{translate('Today')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
!isSmallScreen &&
|
|
||||||
<div className={styles.titleDesktop}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className={styles.viewButtonsContainer}>
|
|
||||||
{
|
|
||||||
isFetching &&
|
|
||||||
<LoadingIndicator
|
|
||||||
className={styles.loading}
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
collapseViewButtons ?
|
|
||||||
<Menu
|
|
||||||
className={styles.viewMenu}
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
>
|
|
||||||
<MenuButton>
|
|
||||||
<Icon
|
|
||||||
name={icons.VIEW}
|
|
||||||
size={22}
|
|
||||||
/>
|
|
||||||
</MenuButton>
|
|
||||||
|
|
||||||
<MenuContent>
|
|
||||||
{
|
|
||||||
isSmallScreen ?
|
|
||||||
null :
|
|
||||||
<ViewMenuItem
|
|
||||||
name={calendarViews.MONTH}
|
|
||||||
selectedView={view}
|
|
||||||
onPress={this.onViewChange}
|
|
||||||
>
|
|
||||||
{translate('Month')}
|
|
||||||
</ViewMenuItem>
|
|
||||||
}
|
|
||||||
|
|
||||||
<ViewMenuItem
|
|
||||||
name={calendarViews.WEEK}
|
|
||||||
selectedView={view}
|
|
||||||
onPress={this.onViewChange}
|
|
||||||
>
|
|
||||||
{translate('Week')}
|
|
||||||
</ViewMenuItem>
|
|
||||||
|
|
||||||
<ViewMenuItem
|
|
||||||
name={calendarViews.FORECAST}
|
|
||||||
selectedView={view}
|
|
||||||
onPress={this.onViewChange}
|
|
||||||
>
|
|
||||||
{translate('Forecast')}
|
|
||||||
</ViewMenuItem>
|
|
||||||
|
|
||||||
<ViewMenuItem
|
|
||||||
name={calendarViews.DAY}
|
|
||||||
selectedView={view}
|
|
||||||
onPress={this.onViewChange}
|
|
||||||
>
|
|
||||||
{translate('Day')}
|
|
||||||
</ViewMenuItem>
|
|
||||||
|
|
||||||
<ViewMenuItem
|
|
||||||
name={calendarViews.AGENDA}
|
|
||||||
selectedView={view}
|
|
||||||
onPress={this.onViewChange}
|
|
||||||
>
|
|
||||||
{translate('Agenda')}
|
|
||||||
</ViewMenuItem>
|
|
||||||
</MenuContent>
|
|
||||||
</Menu> :
|
|
||||||
|
|
||||||
<div className={styles.viewButtons}>
|
|
||||||
<CalendarHeaderViewButton
|
|
||||||
view={calendarViews.MONTH}
|
|
||||||
selectedView={view}
|
|
||||||
buttonGroupPosition={align.LEFT}
|
|
||||||
onPress={this.onViewChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CalendarHeaderViewButton
|
|
||||||
view={calendarViews.WEEK}
|
|
||||||
selectedView={view}
|
|
||||||
buttonGroupPosition={align.CENTER}
|
|
||||||
onPress={this.onViewChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CalendarHeaderViewButton
|
|
||||||
view={calendarViews.FORECAST}
|
|
||||||
selectedView={view}
|
|
||||||
buttonGroupPosition={align.CENTER}
|
|
||||||
onPress={this.onViewChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CalendarHeaderViewButton
|
|
||||||
view={calendarViews.DAY}
|
|
||||||
selectedView={view}
|
|
||||||
buttonGroupPosition={align.CENTER}
|
|
||||||
onPress={this.onViewChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CalendarHeaderViewButton
|
|
||||||
view={calendarViews.AGENDA}
|
|
||||||
selectedView={view}
|
|
||||||
buttonGroupPosition={align.RIGHT}
|
|
||||||
onPress={this.onViewChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarHeader.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
time: PropTypes.string.isRequired,
|
|
||||||
start: PropTypes.string.isRequired,
|
|
||||||
end: PropTypes.string.isRequired,
|
|
||||||
view: PropTypes.oneOf(calendarViews.all).isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
collapseViewButtons: PropTypes.bool.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
onViewChange: PropTypes.func.isRequired,
|
|
||||||
onTodayPress: PropTypes.func.isRequired,
|
|
||||||
onPreviousPress: PropTypes.func.isRequired,
|
|
||||||
onNextPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarHeader;
|
|
||||||
218
frontend/src/Calendar/Header/CalendarHeader.tsx
Normal file
218
frontend/src/Calendar/Header/CalendarHeader.tsx
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Menu from 'Components/Menu/Menu';
|
||||||
|
import MenuButton from 'Components/Menu/MenuButton';
|
||||||
|
import MenuContent from 'Components/Menu/MenuContent';
|
||||||
|
import ViewMenuItem from 'Components/Menu/ViewMenuItem';
|
||||||
|
import { align, icons } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
gotoCalendarNextRange,
|
||||||
|
gotoCalendarPreviousRange,
|
||||||
|
gotoCalendarToday,
|
||||||
|
setCalendarView,
|
||||||
|
} from 'Store/Actions/calendarActions';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import CalendarHeaderViewButton from './CalendarHeaderViewButton';
|
||||||
|
import styles from './CalendarHeader.css';
|
||||||
|
|
||||||
|
function CalendarHeader() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { isFetching, view, time, start, end } = useSelector(
|
||||||
|
(state: AppState) => state.calendar
|
||||||
|
);
|
||||||
|
|
||||||
|
const { isSmallScreen, isLargeScreen } = useSelector(
|
||||||
|
createDimensionsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
const { longDateFormat } = useSelector(createUISettingsSelector());
|
||||||
|
|
||||||
|
const handleViewChange = useCallback(
|
||||||
|
(newView: string) => {
|
||||||
|
dispatch(setCalendarView({ view: newView }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTodayPress = useCallback(() => {
|
||||||
|
dispatch(gotoCalendarToday());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handlePreviousPress = useCallback(() => {
|
||||||
|
dispatch(gotoCalendarPreviousRange());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleNextPress = useCallback(() => {
|
||||||
|
dispatch(gotoCalendarNextRange());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const title = useMemo(() => {
|
||||||
|
const timeMoment = moment(time);
|
||||||
|
const startMoment = moment(start);
|
||||||
|
const endMoment = moment(end);
|
||||||
|
|
||||||
|
if (view === 'day') {
|
||||||
|
return timeMoment.format(longDateFormat);
|
||||||
|
} else if (view === 'month') {
|
||||||
|
return timeMoment.format('MMMM YYYY');
|
||||||
|
}
|
||||||
|
|
||||||
|
let startFormat = 'MMM D YYYY';
|
||||||
|
let endFormat = 'MMM D YYYY';
|
||||||
|
|
||||||
|
if (startMoment.isSame(endMoment, 'month')) {
|
||||||
|
startFormat = 'MMM D';
|
||||||
|
endFormat = 'D YYYY';
|
||||||
|
} else if (startMoment.isSame(endMoment, 'year')) {
|
||||||
|
startFormat = 'MMM D';
|
||||||
|
endFormat = 'MMM D YYYY';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${startMoment.format(startFormat)} \u2014 ${endMoment.format(
|
||||||
|
endFormat
|
||||||
|
)}`;
|
||||||
|
}, [time, start, end, view, longDateFormat]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isSmallScreen ? <div className={styles.titleMobile}>{title}</div> : null}
|
||||||
|
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.navigationButtons}>
|
||||||
|
<Button
|
||||||
|
buttonGroupPosition="left"
|
||||||
|
isDisabled={view === 'agenda'}
|
||||||
|
onPress={handlePreviousPress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.PAGE_PREVIOUS} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
buttonGroupPosition="right"
|
||||||
|
isDisabled={view === 'agenda'}
|
||||||
|
onPress={handleNextPress}
|
||||||
|
>
|
||||||
|
<Icon name={icons.PAGE_NEXT} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={styles.todayButton}
|
||||||
|
isDisabled={view === 'agenda'}
|
||||||
|
onPress={handleTodayPress}
|
||||||
|
>
|
||||||
|
{translate('Today')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isSmallScreen ? null : (
|
||||||
|
<div className={styles.titleDesktop}>{title}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.viewButtonsContainer}>
|
||||||
|
{isFetching ? (
|
||||||
|
<LoadingIndicator className={styles.loading} size={20} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isLargeScreen ? (
|
||||||
|
<Menu className={styles.viewMenu} alignMenu={align.RIGHT}>
|
||||||
|
<MenuButton>
|
||||||
|
<Icon name={icons.VIEW} size={22} />
|
||||||
|
</MenuButton>
|
||||||
|
|
||||||
|
<MenuContent>
|
||||||
|
{isSmallScreen ? null : (
|
||||||
|
<ViewMenuItem
|
||||||
|
name="month"
|
||||||
|
selectedView={view}
|
||||||
|
onPress={handleViewChange}
|
||||||
|
>
|
||||||
|
{translate('Month')}
|
||||||
|
</ViewMenuItem>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ViewMenuItem
|
||||||
|
name="week"
|
||||||
|
selectedView={view}
|
||||||
|
onPress={handleViewChange}
|
||||||
|
>
|
||||||
|
{translate('Week')}
|
||||||
|
</ViewMenuItem>
|
||||||
|
|
||||||
|
<ViewMenuItem
|
||||||
|
name="forecast"
|
||||||
|
selectedView={view}
|
||||||
|
onPress={handleViewChange}
|
||||||
|
>
|
||||||
|
{translate('Forecast')}
|
||||||
|
</ViewMenuItem>
|
||||||
|
|
||||||
|
<ViewMenuItem
|
||||||
|
name="day"
|
||||||
|
selectedView={view}
|
||||||
|
onPress={handleViewChange}
|
||||||
|
>
|
||||||
|
{translate('Day')}
|
||||||
|
</ViewMenuItem>
|
||||||
|
|
||||||
|
<ViewMenuItem
|
||||||
|
name="agenda"
|
||||||
|
selectedView={view}
|
||||||
|
onPress={handleViewChange}
|
||||||
|
>
|
||||||
|
{translate('Agenda')}
|
||||||
|
</ViewMenuItem>
|
||||||
|
</MenuContent>
|
||||||
|
</Menu>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<CalendarHeaderViewButton
|
||||||
|
view="month"
|
||||||
|
selectedView={view}
|
||||||
|
buttonGroupPosition="left"
|
||||||
|
onPress={handleViewChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CalendarHeaderViewButton
|
||||||
|
view="week"
|
||||||
|
selectedView={view}
|
||||||
|
buttonGroupPosition="center"
|
||||||
|
onPress={handleViewChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CalendarHeaderViewButton
|
||||||
|
view="forecast"
|
||||||
|
selectedView={view}
|
||||||
|
buttonGroupPosition="center"
|
||||||
|
onPress={handleViewChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CalendarHeaderViewButton
|
||||||
|
view="day"
|
||||||
|
selectedView={view}
|
||||||
|
buttonGroupPosition="center"
|
||||||
|
onPress={handleViewChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CalendarHeaderViewButton
|
||||||
|
view="agenda"
|
||||||
|
selectedView={view}
|
||||||
|
buttonGroupPosition="right"
|
||||||
|
onPress={handleViewChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarHeader;
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { gotoCalendarNextRange, gotoCalendarPreviousRange, gotoCalendarToday, setCalendarView } from 'Store/Actions/calendarActions';
|
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import CalendarHeader from './CalendarHeader';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar,
|
|
||||||
createDimensionsSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(calendar, dimensions, uiSettings) => {
|
|
||||||
return {
|
|
||||||
isFetching: calendar.isFetching,
|
|
||||||
view: calendar.view,
|
|
||||||
time: calendar.time,
|
|
||||||
start: calendar.start,
|
|
||||||
end: calendar.end,
|
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
|
||||||
collapseViewButtons: dimensions.isLargeScreen,
|
|
||||||
longDateFormat: uiSettings.longDateFormat
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
setCalendarView,
|
|
||||||
gotoCalendarToday,
|
|
||||||
gotoCalendarPreviousRange,
|
|
||||||
gotoCalendarNextRange
|
|
||||||
};
|
|
||||||
|
|
||||||
class CalendarHeaderConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onViewChange = (view) => {
|
|
||||||
this.props.setCalendarView({ view });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTodayPress = () => {
|
|
||||||
this.props.gotoCalendarToday();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPreviousPress = () => {
|
|
||||||
this.props.gotoCalendarPreviousRange();
|
|
||||||
};
|
|
||||||
|
|
||||||
onNextPress = () => {
|
|
||||||
this.props.gotoCalendarNextRange();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<CalendarHeader
|
|
||||||
{...this.props}
|
|
||||||
onViewChange={this.onViewChange}
|
|
||||||
onTodayPress={this.onTodayPress}
|
|
||||||
onPreviousPress={this.onPreviousPress}
|
|
||||||
onNextPress={this.onNextPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarHeaderConnector.propTypes = {
|
|
||||||
setCalendarView: PropTypes.func.isRequired,
|
|
||||||
gotoCalendarToday: PropTypes.func.isRequired,
|
|
||||||
gotoCalendarPreviousRange: PropTypes.func.isRequired,
|
|
||||||
gotoCalendarNextRange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarHeaderConnector);
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import * as calendarViews from 'Calendar/calendarViews';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
|
||||||
// import styles from './CalendarHeaderViewButton.css';
|
|
||||||
|
|
||||||
class CalendarHeaderViewButton extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
this.props.onPress(this.props.view);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
view,
|
|
||||||
selectedView,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
isDisabled={selectedView === view}
|
|
||||||
{...otherProps}
|
|
||||||
onPress={this.onPress}
|
|
||||||
>
|
|
||||||
{titleCase(view)}
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarHeaderViewButton.propTypes = {
|
|
||||||
view: PropTypes.oneOf(calendarViews.all).isRequired,
|
|
||||||
selectedView: PropTypes.oneOf(calendarViews.all).isRequired,
|
|
||||||
onPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarHeaderViewButton;
|
|
||||||
34
frontend/src/Calendar/Header/CalendarHeaderViewButton.tsx
Normal file
34
frontend/src/Calendar/Header/CalendarHeaderViewButton.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { CalendarView } from 'Calendar/calendarViews';
|
||||||
|
import Button, { ButtonProps } from 'Components/Link/Button';
|
||||||
|
import titleCase from 'Utilities/String/titleCase';
|
||||||
|
|
||||||
|
interface CalendarHeaderViewButtonProps
|
||||||
|
extends Omit<ButtonProps, 'children' | 'onPress'> {
|
||||||
|
view: CalendarView;
|
||||||
|
selectedView: CalendarView;
|
||||||
|
onPress: (view: CalendarView) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarHeaderViewButton({
|
||||||
|
view,
|
||||||
|
selectedView,
|
||||||
|
onPress,
|
||||||
|
...otherProps
|
||||||
|
}: CalendarHeaderViewButtonProps) {
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
onPress(view);
|
||||||
|
}, [view, onPress]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
isDisabled={selectedView === view}
|
||||||
|
{...otherProps}
|
||||||
|
onPress={handlePress}
|
||||||
|
>
|
||||||
|
{titleCase(view)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarHeaderViewButton;
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import LegendIconItem from './LegendIconItem';
|
import LegendIconItem from './LegendIconItem';
|
||||||
import LegendItem from './LegendItem';
|
import LegendItem from './LegendItem';
|
||||||
import styles from './Legend.css';
|
import styles from './Legend.css';
|
||||||
|
|
||||||
function Legend(props) {
|
function Legend() {
|
||||||
const {
|
const view = useSelector((state: AppState) => state.calendar.view);
|
||||||
view,
|
const { showCutoffUnmetIcon, fullColorEvents } = useSelector(
|
||||||
showCutoffUnmetIcon,
|
(state: AppState) => state.calendar.options
|
||||||
fullColorEvents,
|
);
|
||||||
colorImpairedMode
|
const { enableColorImpairedMode } = useSelector(createUISettingsSelector());
|
||||||
} = props;
|
|
||||||
|
|
||||||
const iconsToShow = [];
|
const iconsToShow = [];
|
||||||
const isAgendaView = view === 'agenda';
|
const isAgendaView = view === 'agenda';
|
||||||
@@ -37,7 +38,7 @@ function Legend(props) {
|
|||||||
name={translate('DownloadedAndMonitored')}
|
name={translate('DownloadedAndMonitored')}
|
||||||
isAgendaView={isAgendaView}
|
isAgendaView={isAgendaView}
|
||||||
fullColorEvents={fullColorEvents}
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={enableColorImpairedMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LegendItem
|
<LegendItem
|
||||||
@@ -45,7 +46,7 @@ function Legend(props) {
|
|||||||
name={translate('DownloadedButNotMonitored')}
|
name={translate('DownloadedButNotMonitored')}
|
||||||
isAgendaView={isAgendaView}
|
isAgendaView={isAgendaView}
|
||||||
fullColorEvents={fullColorEvents}
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={enableColorImpairedMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -55,7 +56,7 @@ function Legend(props) {
|
|||||||
name={translate('MissingMonitoredAndConsideredAvailable')}
|
name={translate('MissingMonitoredAndConsideredAvailable')}
|
||||||
isAgendaView={isAgendaView}
|
isAgendaView={isAgendaView}
|
||||||
fullColorEvents={fullColorEvents}
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={enableColorImpairedMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LegendItem
|
<LegendItem
|
||||||
@@ -63,7 +64,7 @@ function Legend(props) {
|
|||||||
name={translate('MissingNotMonitored')}
|
name={translate('MissingNotMonitored')}
|
||||||
isAgendaView={isAgendaView}
|
isAgendaView={isAgendaView}
|
||||||
fullColorEvents={fullColorEvents}
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={enableColorImpairedMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ function Legend(props) {
|
|||||||
name={translate('Queued')}
|
name={translate('Queued')}
|
||||||
isAgendaView={isAgendaView}
|
isAgendaView={isAgendaView}
|
||||||
fullColorEvents={fullColorEvents}
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={enableColorImpairedMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LegendItem
|
<LegendItem
|
||||||
@@ -81,25 +82,13 @@ function Legend(props) {
|
|||||||
name={translate('Unreleased')}
|
name={translate('Unreleased')}
|
||||||
isAgendaView={isAgendaView}
|
isAgendaView={isAgendaView}
|
||||||
fullColorEvents={fullColorEvents}
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={enableColorImpairedMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{iconsToShow.length > 0 ? <div>{iconsToShow[0]}</div> : null}
|
||||||
iconsToShow.length > 0 &&
|
|
||||||
<div>
|
|
||||||
{iconsToShow[0]}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Legend.propTypes = {
|
|
||||||
view: PropTypes.string.isRequired,
|
|
||||||
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
|
||||||
fullColorEvents: PropTypes.bool.isRequired,
|
|
||||||
colorImpairedMode: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Legend;
|
export default Legend;
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import Legend from './Legend';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar.options,
|
|
||||||
(state) => state.calendar.view,
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(calendarOptions, view, uiSettings) => {
|
|
||||||
return {
|
|
||||||
...calendarOptions,
|
|
||||||
view,
|
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(Legend);
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import styles from './LegendIconItem.css';
|
|
||||||
|
|
||||||
function LegendIconItem(props) {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
fullColorEvents,
|
|
||||||
icon,
|
|
||||||
kind,
|
|
||||||
tooltip
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.legendIconItem}
|
|
||||||
title={tooltip}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={classNames(
|
|
||||||
styles.icon,
|
|
||||||
fullColorEvents && 'fullColorEvents'
|
|
||||||
)}
|
|
||||||
name={icon}
|
|
||||||
kind={kind}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LegendIconItem.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
fullColorEvents: PropTypes.bool.isRequired,
|
|
||||||
icon: PropTypes.object.isRequired,
|
|
||||||
kind: PropTypes.string.isRequired,
|
|
||||||
tooltip: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LegendIconItem;
|
|
||||||
33
frontend/src/Calendar/Legend/LegendIconItem.tsx
Normal file
33
frontend/src/Calendar/Legend/LegendIconItem.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { FontAwesomeIconProps } from '@fortawesome/react-fontawesome';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import Icon, { IconProps } from 'Components/Icon';
|
||||||
|
import styles from './LegendIconItem.css';
|
||||||
|
|
||||||
|
interface LegendIconItemProps extends Pick<IconProps, 'kind'> {
|
||||||
|
name: string;
|
||||||
|
fullColorEvents: boolean;
|
||||||
|
icon: FontAwesomeIconProps['icon'];
|
||||||
|
tooltip: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LegendIconItem(props: LegendIconItemProps) {
|
||||||
|
const { name, fullColorEvents, icon, kind, tooltip } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.legendIconItem} title={tooltip}>
|
||||||
|
<Icon
|
||||||
|
className={classNames(
|
||||||
|
styles.icon,
|
||||||
|
fullColorEvents && 'fullColorEvents'
|
||||||
|
)}
|
||||||
|
name={icon}
|
||||||
|
kind={kind}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LegendIconItem;
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './LegendItem.css';
|
|
||||||
|
|
||||||
function LegendItem(props) {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
status,
|
|
||||||
isAgendaView,
|
|
||||||
fullColorEvents,
|
|
||||||
colorImpairedMode
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.legendItem,
|
|
||||||
styles[status],
|
|
||||||
colorImpairedMode && 'colorImpaired',
|
|
||||||
fullColorEvents && !isAgendaView && 'fullColor'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
LegendItem.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
isAgendaView: PropTypes.bool.isRequired,
|
|
||||||
fullColorEvents: PropTypes.bool.isRequired,
|
|
||||||
colorImpairedMode: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LegendItem;
|
|
||||||
35
frontend/src/Calendar/Legend/LegendItem.tsx
Normal file
35
frontend/src/Calendar/Legend/LegendItem.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import { CalendarStatus } from 'typings/Calendar';
|
||||||
|
import styles from './LegendItem.css';
|
||||||
|
|
||||||
|
interface LegendItemProps {
|
||||||
|
name: string;
|
||||||
|
status: CalendarStatus;
|
||||||
|
isAgendaView: boolean;
|
||||||
|
fullColorEvents: boolean;
|
||||||
|
colorImpairedMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LegendItem({
|
||||||
|
name,
|
||||||
|
status,
|
||||||
|
isAgendaView,
|
||||||
|
fullColorEvents,
|
||||||
|
colorImpairedMode,
|
||||||
|
}: LegendItemProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.legendItem,
|
||||||
|
styles[status],
|
||||||
|
colorImpairedMode && 'colorImpaired',
|
||||||
|
fullColorEvents && !isAgendaView && 'fullColor'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LegendItem;
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import CalendarOptionsModalContentConnector from './CalendarOptionsModalContentConnector';
|
|
||||||
|
|
||||||
function CalendarOptionsModal(props) {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
onModalClose
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<CalendarOptionsModalContentConnector
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarOptionsModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarOptionsModal;
|
|
||||||
21
frontend/src/Calendar/Options/CalendarOptionsModal.tsx
Normal file
21
frontend/src/Calendar/Options/CalendarOptionsModal.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import CalendarOptionsModalContent from './CalendarOptionsModalContent';
|
||||||
|
|
||||||
|
interface CalendarOptionsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarOptionsModal({
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
}: CalendarOptionsModalProps) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<CalendarOptionsModalContent onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarOptionsModal;
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
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 { firstDayOfWeekOptions, timeFormatOptions, weekColumnOptions } from 'Settings/UI/UISettings';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
class CalendarOptionsModalContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
const {
|
|
||||||
firstDayOfWeek,
|
|
||||||
calendarWeekColumnHeader,
|
|
||||||
timeFormat,
|
|
||||||
enableColorImpairedMode,
|
|
||||||
fullColorEvents
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
firstDayOfWeek,
|
|
||||||
calendarWeekColumnHeader,
|
|
||||||
timeFormat,
|
|
||||||
enableColorImpairedMode,
|
|
||||||
fullColorEvents
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
firstDayOfWeek,
|
|
||||||
calendarWeekColumnHeader,
|
|
||||||
timeFormat,
|
|
||||||
enableColorImpairedMode
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
prevProps.firstDayOfWeek !== firstDayOfWeek ||
|
|
||||||
prevProps.calendarWeekColumnHeader !== calendarWeekColumnHeader ||
|
|
||||||
prevProps.timeFormat !== timeFormat ||
|
|
||||||
prevProps.enableColorImpairedMode !== enableColorImpairedMode
|
|
||||||
) {
|
|
||||||
this.setState({
|
|
||||||
firstDayOfWeek,
|
|
||||||
calendarWeekColumnHeader,
|
|
||||||
timeFormat,
|
|
||||||
enableColorImpairedMode
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onOptionInputChange = ({ name, value }) => {
|
|
||||||
const {
|
|
||||||
dispatchSetCalendarOption
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
dispatchSetCalendarOption({ [name]: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onGlobalInputChange = ({ name, value }) => {
|
|
||||||
const {
|
|
||||||
dispatchSaveUISettings
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const setting = { [name]: value };
|
|
||||||
|
|
||||||
this.setState(setting, () => {
|
|
||||||
dispatchSaveUISettings(setting);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onLinkFocus = (event) => {
|
|
||||||
event.target.select();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
showMovieInformation,
|
|
||||||
showCutoffUnmetIcon,
|
|
||||||
fullColorEvents,
|
|
||||||
onModalClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
firstDayOfWeek,
|
|
||||||
calendarWeekColumnHeader,
|
|
||||||
timeFormat,
|
|
||||||
enableColorImpairedMode
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('CalendarOptions')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<FieldSet legend={translate('Local')}>
|
|
||||||
<Form>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ShowMovieInformation')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showMovieInformation"
|
|
||||||
value={showMovieInformation}
|
|
||||||
helpText={translate('ShowMovieInformationHelpText')}
|
|
||||||
onChange={this.onOptionInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('IconForCutoffUnmet')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="showCutoffUnmetIcon"
|
|
||||||
value={showCutoffUnmetIcon}
|
|
||||||
helpText={translate('IconForCutoffUnmetHelpText')}
|
|
||||||
onChange={this.onOptionInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('FullColorEvents')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="fullColorEvents"
|
|
||||||
value={fullColorEvents}
|
|
||||||
helpText={translate('FullColorEventsHelpText')}
|
|
||||||
onChange={this.onOptionInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
</FieldSet>
|
|
||||||
|
|
||||||
<FieldSet legend={translate('Global')}>
|
|
||||||
<Form>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('FirstDayOfWeek')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="firstDayOfWeek"
|
|
||||||
values={firstDayOfWeekOptions}
|
|
||||||
value={firstDayOfWeek}
|
|
||||||
onChange={this.onGlobalInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('WeekColumnHeader')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="calendarWeekColumnHeader"
|
|
||||||
values={weekColumnOptions}
|
|
||||||
value={calendarWeekColumnHeader}
|
|
||||||
onChange={this.onGlobalInputChange}
|
|
||||||
helpText={translate('WeekColumnHeaderHelpText')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('TimeFormat')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="timeFormat"
|
|
||||||
values={timeFormatOptions}
|
|
||||||
value={timeFormat}
|
|
||||||
onChange={this.onGlobalInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('EnableColorImpairedMode')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="enableColorImpairedMode"
|
|
||||||
value={enableColorImpairedMode}
|
|
||||||
helpText={translate('EnableColorImpairedModeHelpText')}
|
|
||||||
onChange={this.onGlobalInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
</FieldSet>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={onModalClose}>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarOptionsModalContent.propTypes = {
|
|
||||||
showMovieInformation: PropTypes.bool.isRequired,
|
|
||||||
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
|
||||||
firstDayOfWeek: PropTypes.number.isRequired,
|
|
||||||
calendarWeekColumnHeader: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
|
||||||
fullColorEvents: PropTypes.bool.isRequired,
|
|
||||||
dispatchSetCalendarOption: PropTypes.func.isRequired,
|
|
||||||
dispatchSaveUISettings: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarOptionsModalContent;
|
|
||||||
243
frontend/src/Calendar/Options/CalendarOptionsModalContent.tsx
Normal file
243
frontend/src/Calendar/Options/CalendarOptionsModalContent.tsx
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
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 {
|
||||||
|
firstDayOfWeekOptions,
|
||||||
|
timeFormatOptions,
|
||||||
|
weekColumnOptions,
|
||||||
|
} from 'Settings/UI/UISettings';
|
||||||
|
import { setCalendarOption } from 'Store/Actions/calendarActions';
|
||||||
|
import { saveUISettings } from 'Store/Actions/settingsActions';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import UiSettings from 'typings/Settings/UiSettings';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
interface CalendarOptionsModalContentProps {
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarOptionsModalContent({
|
||||||
|
onModalClose,
|
||||||
|
}: CalendarOptionsModalContentProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const {
|
||||||
|
showMovieInformation,
|
||||||
|
showCinemaRelease,
|
||||||
|
showDigitalRelease,
|
||||||
|
showPhysicalRelease,
|
||||||
|
showCutoffUnmetIcon,
|
||||||
|
fullColorEvents,
|
||||||
|
} = useSelector((state: AppState) => state.calendar.options);
|
||||||
|
|
||||||
|
const uiSettings = useSelector(createUISettingsSelector());
|
||||||
|
|
||||||
|
const [state, setState] = useState<Partial<UiSettings>>({
|
||||||
|
firstDayOfWeek: uiSettings.firstDayOfWeek,
|
||||||
|
calendarWeekColumnHeader: uiSettings.calendarWeekColumnHeader,
|
||||||
|
timeFormat: uiSettings.timeFormat,
|
||||||
|
enableColorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
firstDayOfWeek,
|
||||||
|
calendarWeekColumnHeader,
|
||||||
|
timeFormat,
|
||||||
|
enableColorImpairedMode,
|
||||||
|
} = state;
|
||||||
|
|
||||||
|
const handleOptionInputChange = useCallback(
|
||||||
|
({ name, value }: InputChanged) => {
|
||||||
|
dispatch(setCalendarOption({ [name]: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGlobalInputChange = useCallback(
|
||||||
|
({ name, value }: InputChanged) => {
|
||||||
|
setState((prevState) => ({ ...prevState, [name]: value }));
|
||||||
|
|
||||||
|
dispatch(saveUISettings({ [name]: value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setState({
|
||||||
|
firstDayOfWeek: uiSettings.firstDayOfWeek,
|
||||||
|
calendarWeekColumnHeader: uiSettings.calendarWeekColumnHeader,
|
||||||
|
timeFormat: uiSettings.timeFormat,
|
||||||
|
enableColorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||||
|
});
|
||||||
|
}, [uiSettings]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('CalendarOptions')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<FieldSet legend={translate('Local')}>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowMovieInformation')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showMovieInformation"
|
||||||
|
value={showMovieInformation}
|
||||||
|
helpText={translate('ShowMovieInformationHelpText')}
|
||||||
|
onChange={handleOptionInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowCinemaRelease')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showCinemaRelease"
|
||||||
|
value={showCinemaRelease}
|
||||||
|
helpText={translate('ShowCinemaReleaseCalendarHelpText')}
|
||||||
|
isDisabled={
|
||||||
|
showCinemaRelease &&
|
||||||
|
!showDigitalRelease &&
|
||||||
|
!showPhysicalRelease
|
||||||
|
}
|
||||||
|
onChange={handleOptionInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowDigitalRelease')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showDigitalRelease"
|
||||||
|
value={showDigitalRelease}
|
||||||
|
helpText={translate('ShowDigitalReleaseCalendarHelpText')}
|
||||||
|
isDisabled={
|
||||||
|
!showCinemaRelease &&
|
||||||
|
showDigitalRelease &&
|
||||||
|
!showPhysicalRelease
|
||||||
|
}
|
||||||
|
onChange={handleOptionInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ShowPhysicalRelease')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showPhysicalRelease"
|
||||||
|
value={showPhysicalRelease}
|
||||||
|
helpText={translate('ShowPhysicalReleaseCalendarHelpText')}
|
||||||
|
isDisabled={
|
||||||
|
!showCinemaRelease &&
|
||||||
|
!showDigitalRelease &&
|
||||||
|
showPhysicalRelease
|
||||||
|
}
|
||||||
|
onChange={handleOptionInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IconForCutoffUnmet')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="showCutoffUnmetIcon"
|
||||||
|
value={showCutoffUnmetIcon}
|
||||||
|
helpText={translate('IconForCutoffUnmetHelpText')}
|
||||||
|
onChange={handleOptionInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('FullColorEvents')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="fullColorEvents"
|
||||||
|
value={fullColorEvents}
|
||||||
|
helpText={translate('FullColorEventsHelpText')}
|
||||||
|
onChange={handleOptionInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
<FieldSet legend={translate('Global')}>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('FirstDayOfWeek')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="firstDayOfWeek"
|
||||||
|
values={firstDayOfWeekOptions}
|
||||||
|
value={firstDayOfWeek}
|
||||||
|
onChange={handleGlobalInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('WeekColumnHeader')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="calendarWeekColumnHeader"
|
||||||
|
values={weekColumnOptions}
|
||||||
|
value={calendarWeekColumnHeader}
|
||||||
|
helpText={translate('WeekColumnHeaderHelpText')}
|
||||||
|
onChange={handleGlobalInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('TimeFormat')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="timeFormat"
|
||||||
|
values={timeFormatOptions}
|
||||||
|
value={timeFormat}
|
||||||
|
onChange={handleGlobalInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('EnableColorImpairedMode')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="enableColorImpairedMode"
|
||||||
|
value={enableColorImpairedMode}
|
||||||
|
helpText={translate('EnableColorImpairedModeHelpText')}
|
||||||
|
onChange={handleGlobalInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</FieldSet>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarOptionsModalContent;
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { setCalendarOption } from 'Store/Actions/calendarActions';
|
|
||||||
import { saveUISettings } from 'Store/Actions/settingsActions';
|
|
||||||
import CalendarOptionsModalContent from './CalendarOptionsModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar.options,
|
|
||||||
(state) => state.settings.ui.item,
|
|
||||||
(options, uiSettings) => {
|
|
||||||
return {
|
|
||||||
...options,
|
|
||||||
...uiSettings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchSetCalendarOption: setCalendarOption,
|
|
||||||
dispatchSaveUISettings: saveUISettings
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarOptionsModalContent);
|
|
||||||
@@ -5,3 +5,5 @@ export const FORECAST = 'forecast';
|
|||||||
export const AGENDA = 'agenda';
|
export const AGENDA = 'agenda';
|
||||||
|
|
||||||
export const all = [DAY, WEEK, MONTH, FORECAST, AGENDA];
|
export const all = [DAY, WEEK, MONTH, FORECAST, AGENDA];
|
||||||
|
|
||||||
|
export type CalendarView = 'agenda' | 'day' | 'forecast' | 'month' | 'week';
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
function getStatusStyle(hasFile, downloading, isMonitored, isAvailable) {
|
function getStatusStyle(
|
||||||
|
hasFile: boolean,
|
||||||
|
downloading: boolean,
|
||||||
|
isMonitored: boolean,
|
||||||
|
isAvailable: boolean
|
||||||
|
) {
|
||||||
if (downloading) {
|
if (downloading) {
|
||||||
return 'queue';
|
return 'queue';
|
||||||
}
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import CalendarLinkModalContentConnector from './CalendarLinkModalContentConnector';
|
|
||||||
|
|
||||||
function CalendarLinkModal(props) {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
onModalClose
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<CalendarLinkModalContentConnector
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarLinkModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarLinkModal;
|
|
||||||
20
frontend/src/Calendar/iCal/CalendarLinkModal.tsx
Normal file
20
frontend/src/Calendar/iCal/CalendarLinkModal.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import CalendarLinkModalContent from './CalendarLinkModalContent';
|
||||||
|
|
||||||
|
interface CalendarLinkModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarLinkModal(props: CalendarLinkModalProps) {
|
||||||
|
const { isOpen, onModalClose } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<CalendarLinkModalContent onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarLinkModal;
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Form from 'Components/Form/Form';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputButton from 'Components/Form/FormInputButton';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import ClipboardButton from 'Components/Link/ClipboardButton';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { icons, inputTypes, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
function getUrls(state) {
|
|
||||||
const {
|
|
||||||
unmonitored,
|
|
||||||
asAllDay,
|
|
||||||
tags
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v3/calendar/Radarr.ics?`;
|
|
||||||
|
|
||||||
if (unmonitored) {
|
|
||||||
icalUrl += 'unmonitored=true&';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (asAllDay) {
|
|
||||||
icalUrl += 'asAllDay=true&';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tags.length) {
|
|
||||||
icalUrl += `tags=${tags.toString()}&`;
|
|
||||||
}
|
|
||||||
|
|
||||||
icalUrl += `apikey=${encodeURIComponent(window.Radarr.apiKey)}`;
|
|
||||||
|
|
||||||
const iCalHttpUrl = `${window.location.protocol}//${icalUrl}`;
|
|
||||||
const iCalWebCalUrl = `webcal://${icalUrl}`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
iCalHttpUrl,
|
|
||||||
iCalWebCalUrl
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class CalendarLinkModalContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
const defaultState = {
|
|
||||||
unmonitored: false,
|
|
||||||
asAllDay: false,
|
|
||||||
tags: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const urls = getUrls(defaultState);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
...defaultState,
|
|
||||||
...urls
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
const state = {
|
|
||||||
...this.state,
|
|
||||||
[name]: value
|
|
||||||
};
|
|
||||||
|
|
||||||
const urls = getUrls(state);
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
[name]: value,
|
|
||||||
...urls
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onLinkFocus = (event) => {
|
|
||||||
event.target.select();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
onModalClose
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
unmonitored,
|
|
||||||
asAllDay,
|
|
||||||
tags,
|
|
||||||
iCalHttpUrl,
|
|
||||||
iCalWebCalUrl
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('CalendarFeed')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<Form>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('IncludeUnmonitored')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="unmonitored"
|
|
||||||
value={unmonitored}
|
|
||||||
helpText={translate('ICalIncludeUnmonitoredMoviesHelpText')}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('ICalShowAsAllDayEvents')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="asAllDay"
|
|
||||||
value={asAllDay}
|
|
||||||
helpText={translate('ICalShowAsAllDayEventsHelpText')}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Tags')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TAG}
|
|
||||||
name="tags"
|
|
||||||
value={tags}
|
|
||||||
helpText={translate('ICalTagsMoviesHelpText')}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup
|
|
||||||
size={sizes.LARGE}
|
|
||||||
>
|
|
||||||
<FormLabel>{translate('ICalFeed')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TEXT}
|
|
||||||
name="iCalHttpUrl"
|
|
||||||
value={iCalHttpUrl}
|
|
||||||
readOnly={true}
|
|
||||||
helpText={translate('ICalFeedHelpText')}
|
|
||||||
buttons={[
|
|
||||||
<ClipboardButton
|
|
||||||
key="copy"
|
|
||||||
value={iCalHttpUrl}
|
|
||||||
kind={kinds.DEFAULT}
|
|
||||||
/>,
|
|
||||||
|
|
||||||
<FormInputButton
|
|
||||||
key="webcal"
|
|
||||||
kind={kinds.DEFAULT}
|
|
||||||
to={iCalWebCalUrl}
|
|
||||||
target="_blank"
|
|
||||||
noRouter={true}
|
|
||||||
>
|
|
||||||
<Icon name={icons.CALENDAR_O} />
|
|
||||||
</FormInputButton>
|
|
||||||
]}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
onFocus={this.onLinkFocus}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={onModalClose}>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarLinkModalContent.propTypes = {
|
|
||||||
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarLinkModalContent;
|
|
||||||
196
frontend/src/Calendar/iCal/CalendarLinkModalContent.tsx
Normal file
196
frontend/src/Calendar/iCal/CalendarLinkModalContent.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import React, { FocusEvent, useCallback, useMemo, useState } from 'react';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputButton from 'Components/Form/FormInputButton';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import ClipboardButton from 'Components/Link/ClipboardButton';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { icons, inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
const releaseTypeOptions: EnhancedSelectInputValue<string>[] = [
|
||||||
|
{
|
||||||
|
key: 'cinemaRelease',
|
||||||
|
get value() {
|
||||||
|
return translate('CinemaRelease');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'digitalRelease',
|
||||||
|
get value() {
|
||||||
|
return translate('DigitalRelease');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'physicalRelease',
|
||||||
|
get value() {
|
||||||
|
return translate('PhysicalRelease');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface CalendarLinkModalContentProps {
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarLinkModalContent({
|
||||||
|
onModalClose,
|
||||||
|
}: CalendarLinkModalContentProps) {
|
||||||
|
const [state, setState] = useState<{
|
||||||
|
unmonitored: boolean;
|
||||||
|
asAllDay: boolean;
|
||||||
|
releaseTypes: string[];
|
||||||
|
tags: number[];
|
||||||
|
}>({
|
||||||
|
unmonitored: false,
|
||||||
|
asAllDay: false,
|
||||||
|
releaseTypes: [],
|
||||||
|
tags: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { unmonitored, asAllDay, releaseTypes, tags } = state;
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(({ name, value }: InputChanged) => {
|
||||||
|
setState((prevState) => ({ ...prevState, [name]: value }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLinkFocus = useCallback(
|
||||||
|
(event: FocusEvent<HTMLInputElement, Element>) => {
|
||||||
|
event.target.select();
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { iCalHttpUrl, iCalWebCalUrl } = useMemo(() => {
|
||||||
|
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v3/calendar/Radarr.ics?`;
|
||||||
|
|
||||||
|
if (unmonitored) {
|
||||||
|
icalUrl += 'unmonitored=true&';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asAllDay) {
|
||||||
|
icalUrl += 'asAllDay=true&';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releaseTypes.length) {
|
||||||
|
releaseTypes.forEach((releaseType) => {
|
||||||
|
icalUrl += `releaseTypes=${releaseType}&`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tags.length) {
|
||||||
|
icalUrl += `tags=${tags.toString()}&`;
|
||||||
|
}
|
||||||
|
|
||||||
|
icalUrl += `apikey=${encodeURIComponent(window.Radarr.apiKey)}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
iCalHttpUrl: `${window.location.protocol}//${icalUrl}`,
|
||||||
|
iCalWebCalUrl: `webcal://${icalUrl}`,
|
||||||
|
};
|
||||||
|
}, [unmonitored, asAllDay, releaseTypes, tags]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('CalendarFeed')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('IncludeUnmonitored')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="unmonitored"
|
||||||
|
value={unmonitored}
|
||||||
|
helpText={translate('ICalIncludeUnmonitoredMoviesHelpText')}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ICalShowAsAllDayEvents')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="asAllDay"
|
||||||
|
value={asAllDay}
|
||||||
|
helpText={translate('ICalShowAsAllDayEventsHelpText')}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('ICalReleaseTypes')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="releaseTypes"
|
||||||
|
value={releaseTypes}
|
||||||
|
values={releaseTypeOptions}
|
||||||
|
helpText={translate('ICalReleaseTypesMoviesHelpText')}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.MOVIE_TAG}
|
||||||
|
name="tags"
|
||||||
|
value={tags}
|
||||||
|
helpText={translate('ICalTagsMoviesHelpText')}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup size={sizes.LARGE}>
|
||||||
|
<FormLabel>{translate('ICalFeed')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="iCalHttpUrl"
|
||||||
|
value={iCalHttpUrl}
|
||||||
|
readOnly={true}
|
||||||
|
helpText={translate('ICalFeedHelpText')}
|
||||||
|
buttons={[
|
||||||
|
<ClipboardButton
|
||||||
|
key="copy"
|
||||||
|
value={iCalHttpUrl}
|
||||||
|
kind={kinds.DEFAULT}
|
||||||
|
/>,
|
||||||
|
|
||||||
|
<FormInputButton
|
||||||
|
key="webcal"
|
||||||
|
kind={kinds.DEFAULT}
|
||||||
|
to={iCalWebCalUrl}
|
||||||
|
target="_blank"
|
||||||
|
noRouter={true}
|
||||||
|
>
|
||||||
|
<Icon name={icons.CALENDAR_O} />
|
||||||
|
</FormInputButton>,
|
||||||
|
]}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onFocus={handleLinkFocus}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarLinkModalContent;
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createTagsSelector from 'Store/Selectors/createTagsSelector';
|
|
||||||
import CalendarLinkModalContent from './CalendarLinkModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createTagsSelector(),
|
|
||||||
(tagList) => {
|
|
||||||
return {
|
|
||||||
tagList
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(CalendarLinkModalContent);
|
|
||||||
@@ -196,7 +196,7 @@ class CollectionOverview extends Component {
|
|||||||
size={13}
|
size={13}
|
||||||
/>
|
/>
|
||||||
<span className={styles.status}>
|
<span className={styles.status}>
|
||||||
{`${missingMovies} missing movie(s)`}
|
{translate('CountMissingMoviesFromLibrary', { count: missingMovies })}
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ function CustomFiltersModalContent(props) {
|
|||||||
{translate('AddCustomFilter')}
|
{translate('AddCustomFilter')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<br />
|
||||||
|
{translate('FilterMoviePropertiesOnlyNotFileWarning')}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -5,18 +5,20 @@ import { ValidationError, ValidationWarning } from 'typings/pending';
|
|||||||
import styles from './Form.css';
|
import styles from './Form.css';
|
||||||
|
|
||||||
export interface FormProps {
|
export interface FormProps {
|
||||||
|
id?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
validationErrors?: ValidationError[];
|
validationErrors?: ValidationError[];
|
||||||
validationWarnings?: ValidationWarning[];
|
validationWarnings?: ValidationWarning[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function Form({
|
function Form({
|
||||||
|
id,
|
||||||
children,
|
children,
|
||||||
validationErrors = [],
|
validationErrors = [],
|
||||||
validationWarnings = [],
|
validationWarnings = [],
|
||||||
}: FormProps) {
|
}: FormProps) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div id={id}>
|
||||||
{validationErrors.length || validationWarnings.length ? (
|
{validationErrors.length || validationWarnings.length ? (
|
||||||
<div className={styles.validationFailures}>
|
<div className={styles.validationFailures}>
|
||||||
{validationErrors.map((error, index) => {
|
{validationErrors.map((error, index) => {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function createQualityProfilesSelector(
|
|||||||
includeMixed: boolean
|
includeMixed: boolean
|
||||||
) {
|
) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createSortedSectionSelector(
|
createSortedSectionSelector<QualityProfile, QualityProfilesAppState>(
|
||||||
'settings.qualityProfiles',
|
'settings.qualityProfiles',
|
||||||
sortByProp<QualityProfile, 'name'>('name')
|
sortByProp<QualityProfile, 'name'>('name')
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -26,6 +26,10 @@
|
|||||||
color: var(--warningColor);
|
color: var(--warningColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.primary {
|
||||||
|
color: var(--primaryColor);
|
||||||
|
}
|
||||||
|
|
||||||
.purple {
|
.purple {
|
||||||
color: var(--purple);
|
color: var(--purple);
|
||||||
}
|
}
|
||||||
|
|||||||
1
frontend/src/Components/Icon.css.d.ts
vendored
1
frontend/src/Components/Icon.css.d.ts
vendored
@@ -6,6 +6,7 @@ interface CssExports {
|
|||||||
'disabled': string;
|
'disabled': string;
|
||||||
'info': string;
|
'info': string;
|
||||||
'pink': string;
|
'pink': string;
|
||||||
|
'primary': string;
|
||||||
'purple': string;
|
'purple': string;
|
||||||
'success': string;
|
'success': string;
|
||||||
'warning': string;
|
'warning': string;
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { align, kinds, sizes } from 'Helpers/Props';
|
import { kinds, sizes } from 'Helpers/Props';
|
||||||
|
import { Align } from 'Helpers/Props/align';
|
||||||
import { Kind } from 'Helpers/Props/kinds';
|
import { Kind } from 'Helpers/Props/kinds';
|
||||||
import { Size } from 'Helpers/Props/sizes';
|
import { Size } from 'Helpers/Props/sizes';
|
||||||
import Link, { LinkProps } from './Link';
|
import Link, { LinkProps } from './Link';
|
||||||
import styles from './Button.css';
|
import styles from './Button.css';
|
||||||
|
|
||||||
export interface ButtonProps extends Omit<LinkProps, 'children' | 'size'> {
|
export interface ButtonProps extends Omit<LinkProps, 'children' | 'size'> {
|
||||||
buttonGroupPosition?: Extract<
|
buttonGroupPosition?: Extract<Align, keyof typeof styles>;
|
||||||
(typeof align.all)[number],
|
|
||||||
keyof typeof styles
|
|
||||||
>;
|
|
||||||
kind?: Extract<Kind, keyof typeof styles>;
|
kind?: Extract<Kind, keyof typeof styles>;
|
||||||
size?: Extract<Size, keyof typeof styles>;
|
size?: Extract<Size, keyof typeof styles>;
|
||||||
children: Required<LinkProps['children']>;
|
children: Required<LinkProps['children']>;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Error } from 'App/State/AppSectionState';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
@@ -6,7 +7,7 @@ import { kinds } from 'Helpers/Props';
|
|||||||
interface PageSectionContentProps {
|
interface PageSectionContentProps {
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isPopulated: boolean;
|
isPopulated: boolean;
|
||||||
error?: object;
|
error?: Error;
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
@@ -18,7 +19,7 @@ function PageSectionContent({
|
|||||||
errorMessage,
|
errorMessage,
|
||||||
children,
|
children,
|
||||||
}: PageSectionContentProps) {
|
}: PageSectionContentProps) {
|
||||||
if (isFetching) {
|
if (isFetching && !isPopulated) {
|
||||||
return <LoadingIndicator />;
|
return <LoadingIndicator />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ const BUTTON_WIDTH = parseInt(dimensions.toolbarButtonWidth);
|
|||||||
const SEPARATOR_MARGIN = parseInt(dimensions.toolbarSeparatorMargin);
|
const SEPARATOR_MARGIN = parseInt(dimensions.toolbarSeparatorMargin);
|
||||||
const SEPARATOR_WIDTH = 2 * SEPARATOR_MARGIN + 1;
|
const SEPARATOR_WIDTH = 2 * SEPARATOR_MARGIN + 1;
|
||||||
|
|
||||||
interface PageToolbarSectionProps {
|
export interface PageToolbarSectionProps {
|
||||||
children?:
|
children?:
|
||||||
| (ReactElement<PageToolbarButtonProps> | ReactElement<never>)
|
| (ReactElement<PageToolbarButtonProps> | ReactElement<never> | null)
|
||||||
| (ReactElement<PageToolbarButtonProps> | ReactElement<never>)[];
|
| (ReactElement<PageToolbarButtonProps> | ReactElement<never> | null)[];
|
||||||
alignContent?: Extract<Align, keyof typeof styles>;
|
alignContent?: Extract<Align, keyof typeof styles>;
|
||||||
collapseButtons?: boolean;
|
collapseButtons?: boolean;
|
||||||
}
|
}
|
||||||
@@ -80,8 +80,12 @@ function PageToolbarSection({
|
|||||||
if (buttonCount - 1 === maxButtons) {
|
if (buttonCount - 1 === maxButtons) {
|
||||||
const overflowItems: PageToolbarButtonProps[] = [];
|
const overflowItems: PageToolbarButtonProps[] = [];
|
||||||
|
|
||||||
|
const buttonsWithoutSeparators = validChildren.filter(
|
||||||
|
(child) => Object.keys(child.props).length > 0
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
buttons: validChildren,
|
buttons: buttonsWithoutSeparators,
|
||||||
buttonCount,
|
buttonCount,
|
||||||
overflowItems,
|
overflowItems,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ function DiscoverMovieSortMenu(props) {
|
|||||||
{translate('Studio')}
|
{translate('Studio')}
|
||||||
</SortMenuItem>
|
</SortMenuItem>
|
||||||
|
|
||||||
|
<SortMenuItem
|
||||||
|
name="year"
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onPress={onSortSelect}
|
||||||
|
>
|
||||||
|
{translate('Year')}
|
||||||
|
</SortMenuItem>
|
||||||
|
|
||||||
<SortMenuItem
|
<SortMenuItem
|
||||||
name="inCinemas"
|
name="inCinemas"
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
|
|||||||
@@ -36,7 +36,8 @@
|
|||||||
.imdbRating,
|
.imdbRating,
|
||||||
.rottenTomatoesRating,
|
.rottenTomatoesRating,
|
||||||
.traktRating,
|
.traktRating,
|
||||||
.runtime {
|
.runtime,
|
||||||
|
.year {
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
flex: 0 0 90px;
|
flex: 0 0 90px;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ interface CssExports {
|
|||||||
'studio': string;
|
'studio': string;
|
||||||
'tmdbRating': string;
|
'tmdbRating': string;
|
||||||
'traktRating': string;
|
'traktRating': string;
|
||||||
|
'year': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -61,7 +61,8 @@
|
|||||||
.imdbRating,
|
.imdbRating,
|
||||||
.rottenTomatoesRating,
|
.rottenTomatoesRating,
|
||||||
.traktRating,
|
.traktRating,
|
||||||
.runtime {
|
.runtime,
|
||||||
|
.year {
|
||||||
composes: cell;
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 90px;
|
flex: 0 0 90px;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ interface CssExports {
|
|||||||
'studio': string;
|
'studio': string;
|
||||||
'tmdbRating': string;
|
'tmdbRating': string;
|
||||||
'traktRating': string;
|
'traktRating': string;
|
||||||
|
'year': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -167,6 +167,14 @@ class DiscoverMovieRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'year') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
{year}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'collection') {
|
if (name === 'collection') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell
|
<VirtualTableRowCell
|
||||||
|
|||||||
8
frontend/src/Helpers/Hooks/useIsWindows.ts
Normal file
8
frontend/src/Helpers/Hooks/useIsWindows.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
|
function useIsWindows() {
|
||||||
|
return useSelector((state: AppState) => state.system.status.item.isWindows);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useIsWindows;
|
||||||
8
frontend/src/Helpers/Hooks/useShowAdvancedSettings.ts
Normal file
8
frontend/src/Helpers/Hooks/useShowAdvancedSettings.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
|
function useShowAdvancedSettings() {
|
||||||
|
return useSelector((state: AppState) => state.settings.advancedSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useShowAdvancedSettings;
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
// https://github.com/react-bootstrap/react-element-children
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterates through children that are typically specified as `props.children`,
|
|
||||||
* but only maps over children that are "valid components".
|
|
||||||
*
|
|
||||||
* The mapFunction provided index will be normalised to the components mapped,
|
|
||||||
* so an invalid component would not increase the index.
|
|
||||||
*
|
|
||||||
* @param {?*} children Children tree container.
|
|
||||||
* @param {function(*, int)} func.
|
|
||||||
* @param {*} context Context for func.
|
|
||||||
* @return {object} Object containing the ordered map of results.
|
|
||||||
*/
|
|
||||||
export function map(children, func, context) {
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
return React.Children.map(children, (child) => {
|
|
||||||
if (!React.isValidElement(child)) {
|
|
||||||
return child;
|
|
||||||
}
|
|
||||||
|
|
||||||
return func.call(context, child, index++);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Iterates through children that are "valid components".
|
|
||||||
*
|
|
||||||
* The provided forEachFunc(child, index) will be called for each
|
|
||||||
* leaf child with the index reflecting the position relative to "valid components".
|
|
||||||
*
|
|
||||||
* @param {?*} children Children tree container.
|
|
||||||
* @param {function(*, int)} func.
|
|
||||||
* @param {*} context Context for context.
|
|
||||||
*/
|
|
||||||
export function forEach(children, func, context) {
|
|
||||||
let index = 0;
|
|
||||||
|
|
||||||
React.Children.forEach(children, (child) => {
|
|
||||||
if (!React.isValidElement(child)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
func.call(context, child, index++);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Count the number of "valid components" in the Children container.
|
|
||||||
*
|
|
||||||
* @param {?*} children Children tree container.
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
export function count(children) {
|
|
||||||
let result = 0;
|
|
||||||
|
|
||||||
React.Children.forEach(children, (child) => {
|
|
||||||
if (!React.isValidElement(child)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
++result;
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds children that are typically specified as `props.children`,
|
|
||||||
* but only iterates over children that are "valid components".
|
|
||||||
*
|
|
||||||
* The provided forEachFunc(child, index) will be called for each
|
|
||||||
* leaf child with the index reflecting the position relative to "valid components".
|
|
||||||
*
|
|
||||||
* @param {?*} children Children tree container.
|
|
||||||
* @param {function(*, int)} func.
|
|
||||||
* @param {*} context Context for func.
|
|
||||||
* @returns {array} of children that meet the func return statement
|
|
||||||
*/
|
|
||||||
export function filter(children, func, context) {
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
forEach(children, (child, index) => {
|
|
||||||
if (func.call(context, child, index)) {
|
|
||||||
result.push(child);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function find(children, func, context) {
|
|
||||||
let result = null;
|
|
||||||
|
|
||||||
forEach(children, (child, index) => {
|
|
||||||
if (result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (func.call(context, child, index)) {
|
|
||||||
result = child;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function every(children, func, context) {
|
|
||||||
let result = true;
|
|
||||||
|
|
||||||
forEach(children, (child, index) => {
|
|
||||||
if (!result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!func.call(context, child, index)) {
|
|
||||||
result = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function some(children, func, context) {
|
|
||||||
let result = false;
|
|
||||||
|
|
||||||
forEach(children, (child, index) => {
|
|
||||||
if (result) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (func.call(context, child, index)) {
|
|
||||||
result = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toArray(children) {
|
|
||||||
const result = [];
|
|
||||||
|
|
||||||
forEach(children, (child) => {
|
|
||||||
result.push(child);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default function getDisplayName(Component) {
|
|
||||||
return Component.displayName || Component.name || 'Component';
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ import Popover from 'Components/Tooltip/Popover';
|
|||||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
import MovieHeadshot from 'Movie/MovieHeadshot';
|
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
import EditImportListModal from 'Settings/ImportLists/ImportLists/EditImportListModal';
|
||||||
import { deleteImportList } from 'Store/Actions/Settings/importLists';
|
import { deleteImportList } from 'Store/Actions/Settings/importLists';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
import MovieCredit from 'typings/MovieCredit';
|
import MovieCredit from 'typings/MovieCredit';
|
||||||
@@ -154,7 +154,7 @@ function MovieCastPoster(props: MovieCastPosterProps) {
|
|||||||
{character}
|
{character}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EditImportListModalConnector
|
<EditImportListModal
|
||||||
id={importListId}
|
id={importListId}
|
||||||
isOpen={isEditImportListModalOpen}
|
isOpen={isEditImportListModalOpen}
|
||||||
onModalClose={setEditImportListModalClosed}
|
onModalClose={setEditImportListModalClosed}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import Popover from 'Components/Tooltip/Popover';
|
|||||||
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
import MovieHeadshot from 'Movie/MovieHeadshot';
|
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
import EditImportListModal from 'Settings/ImportLists/ImportLists/EditImportListModal';
|
||||||
import { deleteImportList } from 'Store/Actions/Settings/importLists';
|
import { deleteImportList } from 'Store/Actions/Settings/importLists';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
import MovieCredit from 'typings/MovieCredit';
|
import MovieCredit from 'typings/MovieCredit';
|
||||||
@@ -152,7 +152,7 @@ function MovieCrewPoster(props: MovieCrewPosterProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className={classNames(styles.title, 'swiper-no-swiping')}>{job}</div>
|
<div className={classNames(styles.title, 'swiper-no-swiping')}>{job}</div>
|
||||||
|
|
||||||
<EditImportListModalConnector
|
<EditImportListModal
|
||||||
id={importListId}
|
id={importListId}
|
||||||
isOpen={isEditImportListModalOpen}
|
isOpen={isEditImportListModalOpen}
|
||||||
onModalClose={setEditImportListModalClosed}
|
onModalClose={setEditImportListModalClosed}
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ const MovieIndex = withScrollPosition((props: MovieIndexProps) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<MovieIndexSelectAllButton
|
<MovieIndexSelectAllButton
|
||||||
label="SelectAll"
|
label={translate('SelectAll')}
|
||||||
isSelectMode={isSelectMode}
|
isSelectMode={isSelectMode}
|
||||||
overflowComponent={MovieIndexSelectAllMenuItem}
|
overflowComponent={MovieIndexSelectAllMenuItem}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { SyntheticEvent, useCallback, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useSelect } from 'App/SelectContext';
|
||||||
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
|
import { MOVIE_SEARCH, REFRESH_MOVIE } from 'Commands/commandNames';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import ImdbRating from 'Components/ImdbRating';
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
@@ -141,8 +142,31 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
setIsDeleteMovieModalOpen(false);
|
setIsDeleteMovieModalOpen(false);
|
||||||
}, [setIsDeleteMovieModalOpen]);
|
}, [setIsDeleteMovieModalOpen]);
|
||||||
|
|
||||||
|
const [selectState, selectDispatch] = useSelect();
|
||||||
|
|
||||||
|
const onSelectPress = useCallback(
|
||||||
|
(event: SyntheticEvent<HTMLElement, MouseEvent>) => {
|
||||||
|
if (event.nativeEvent.ctrlKey || event.nativeEvent.metaKey) {
|
||||||
|
window.open(`/movie/${tmdbId}`, '_blank');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shiftKey = event.nativeEvent.shiftKey;
|
||||||
|
|
||||||
|
selectDispatch({
|
||||||
|
type: 'toggleSelected',
|
||||||
|
id: movieId,
|
||||||
|
isSelected: !selectState.selectedState[movieId],
|
||||||
|
shiftKey,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[movieId, selectState.selectedState, selectDispatch, tmdbId]
|
||||||
|
);
|
||||||
|
|
||||||
const link = `/movie/${tmdbId}`;
|
const link = `/movie/${tmdbId}`;
|
||||||
|
|
||||||
|
const linkProps = isSelectMode ? { onPress: onSelectPress } : { to: link };
|
||||||
|
|
||||||
const elementStyle = {
|
const elementStyle = {
|
||||||
width: `${posterWidth}px`,
|
width: `${posterWidth}px`,
|
||||||
height: `${posterHeight}px`,
|
height: `${posterHeight}px`,
|
||||||
@@ -196,7 +220,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
|||||||
<div className={styles.deleted} title={translate('Deleted')} />
|
<div className={styles.deleted} title={translate('Deleted')} />
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Link className={styles.link} style={elementStyle} to={link}>
|
<Link className={styles.link} style={elementStyle} {...linkProps}>
|
||||||
<MoviePoster
|
<MoviePoster
|
||||||
style={elementStyle}
|
style={elementStyle}
|
||||||
images={images}
|
images={images}
|
||||||
|
|||||||
@@ -140,37 +140,37 @@ function MovieIndexPosterOptionsModalContent(
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
<FormLabel>{translate('ShowCinemaRelease')}</FormLabel>
|
<FormLabel>{translate('ShowCinemaReleaseDate')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="showCinemaRelease"
|
name="showCinemaRelease"
|
||||||
value={showCinemaRelease}
|
value={showCinemaRelease}
|
||||||
helpText={translate('ShowCinemaReleaseHelpText')}
|
helpText={translate('ShowCinemaReleaseDatePosterHelpText')}
|
||||||
onChange={onPosterOptionChange}
|
onChange={onPosterOptionChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
<FormLabel>{translate('ShowDigitalRelease')}</FormLabel>
|
<FormLabel>{translate('ShowDigitalReleaseDate')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="showDigitalRelease"
|
name="showDigitalRelease"
|
||||||
value={showDigitalRelease}
|
value={showDigitalRelease}
|
||||||
helpText={translate('ShowDigitalReleaseHelpText')}
|
helpText={translate('ShowDigitalReleaseDatePosterHelpText')}
|
||||||
onChange={onPosterOptionChange}
|
onChange={onPosterOptionChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup size={sizes.MEDIUM}>
|
<FormGroup size={sizes.MEDIUM}>
|
||||||
<FormLabel>{translate('ShowPhysicalRelease')}</FormLabel>
|
<FormLabel>{translate('ShowPhysicalReleaseDate')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="showPhysicalRelease"
|
name="showPhysicalRelease"
|
||||||
value={showPhysicalRelease}
|
value={showPhysicalRelease}
|
||||||
helpText={translate('ShowPhysicalReleaseHelpText')}
|
helpText={translate('ShowPhysicalReleaseDatePosterHelpText')}
|
||||||
onChange={onPosterOptionChange}
|
onChange={onPosterOptionChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -182,7 +182,7 @@ function MovieIndexPosterOptionsModalContent(
|
|||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="showReleaseDate"
|
name="showReleaseDate"
|
||||||
value={showReleaseDate}
|
value={showReleaseDate}
|
||||||
helpText={translate('ShowReleaseDateHelpText')}
|
helpText={translate('ShowReleaseDatePosterHelpText')}
|
||||||
onChange={onPosterOptionChange}
|
onChange={onPosterOptionChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|||||||
@@ -43,7 +43,8 @@
|
|||||||
.physicalRelease,
|
.physicalRelease,
|
||||||
.digitalRelease,
|
.digitalRelease,
|
||||||
.releaseDate,
|
.releaseDate,
|
||||||
.genres {
|
.genres,
|
||||||
|
.keywords {
|
||||||
composes: cell;
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 180px;
|
flex: 0 0 180px;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ interface CssExports {
|
|||||||
'genres': string;
|
'genres': string;
|
||||||
'imdbRating': string;
|
'imdbRating': string;
|
||||||
'inCinemas': string;
|
'inCinemas': string;
|
||||||
|
'keywords': string;
|
||||||
'minimumAvailability': string;
|
'minimumAvailability': string;
|
||||||
'movieStatus': string;
|
'movieStatus': string;
|
||||||
'originalLanguage': string;
|
'originalLanguage': string;
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
|||||||
minimumAvailability,
|
minimumAvailability,
|
||||||
path,
|
path,
|
||||||
genres = [],
|
genres = [],
|
||||||
|
keywords = [],
|
||||||
ratings,
|
ratings,
|
||||||
popularity,
|
popularity,
|
||||||
certification,
|
certification,
|
||||||
@@ -339,6 +340,20 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'keywords') {
|
||||||
|
const joinedKeywords = keywords.join(', ');
|
||||||
|
const truncatedKeywords =
|
||||||
|
keywords.length > 3
|
||||||
|
? `${keywords.slice(0, 3).join(', ')}...`
|
||||||
|
: joinedKeywords;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
<span title={joinedKeywords}>{truncatedKeywords}</span>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'movieStatus') {
|
if (name === 'movieStatus') {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||||
|
|||||||
@@ -36,7 +36,8 @@
|
|||||||
.physicalRelease,
|
.physicalRelease,
|
||||||
.digitalRelease,
|
.digitalRelease,
|
||||||
.releaseDate,
|
.releaseDate,
|
||||||
.genres {
|
.genres,
|
||||||
|
.keywords {
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
flex: 0 0 180px;
|
flex: 0 0 180px;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface CssExports {
|
|||||||
'genres': string;
|
'genres': string;
|
||||||
'imdbRating': string;
|
'imdbRating': string;
|
||||||
'inCinemas': string;
|
'inCinemas': string;
|
||||||
|
'keywords': string;
|
||||||
'minimumAvailability': string;
|
'minimumAvailability': string;
|
||||||
'movieStatus': string;
|
'movieStatus': string;
|
||||||
'originalLanguage': string;
|
'originalLanguage': string;
|
||||||
|
|||||||
@@ -82,12 +82,14 @@ interface Movie extends ModelBase {
|
|||||||
minimumAvailability: MovieAvailability;
|
minimumAvailability: MovieAvailability;
|
||||||
path: string;
|
path: string;
|
||||||
genres: string[];
|
genres: string[];
|
||||||
|
keywords: string[];
|
||||||
ratings: Ratings;
|
ratings: Ratings;
|
||||||
popularity: number;
|
popularity: number;
|
||||||
certification: string;
|
certification: string;
|
||||||
statistics?: Statistics;
|
statistics?: Statistics;
|
||||||
tags: number[];
|
tags: number[];
|
||||||
images: Image[];
|
images: Image[];
|
||||||
|
movieFileId: number;
|
||||||
movieFile?: MovieFile;
|
movieFile?: MovieFile;
|
||||||
hasFile: boolean;
|
hasFile: boolean;
|
||||||
grabbed?: boolean;
|
grabbed?: boolean;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user