mirror of
https://github.com/Radarr/Radarr
synced 2025-12-06 16:32:36 +01:00
Compare commits
254 commits
v5.22.1.98
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b59ff0a3b1 | ||
|
|
b9c2563c9b | ||
|
|
949922b9a1 | ||
|
|
1b9662d588 | ||
|
|
005c870f69 | ||
|
|
90cd8df1ae | ||
|
|
7d8444c435 | ||
|
|
1883ae52ac | ||
|
|
47d4ebbeac | ||
|
|
ef9836d71d | ||
|
|
955ee2f29b | ||
|
|
abf3fc4557 | ||
|
|
1e72cc6b5a | ||
|
|
24639a7016 | ||
|
|
e52547fa37 | ||
|
|
ff6a69701f | ||
|
|
f6afbfa684 | ||
|
|
b1b33e0dbf | ||
|
|
cf465899b4 | ||
|
|
e63691935d | ||
|
|
1bae9499e4 | ||
|
|
c991a8927d | ||
|
|
3c75250c08 | ||
|
|
1e06fc5b43 | ||
|
|
52307038af | ||
|
|
0297dba7f9 | ||
|
|
554a54b009 | ||
|
|
64b2a10b3f | ||
|
|
97c226c23c | ||
|
|
9959c658be | ||
|
|
eaeb668eb5 | ||
|
|
bb6713f1d2 | ||
|
|
9906b95893 | ||
|
|
8c94581cb6 | ||
|
|
6bdbc9c600 | ||
|
|
f28691e48d | ||
|
|
e7bddaeedd | ||
|
|
94ced8cff9 | ||
|
|
3429fe0696 | ||
|
|
100e121afc | ||
|
|
24be516fdb | ||
|
|
f49c35563d | ||
|
|
6e23750705 | ||
|
|
30fc50e049 | ||
|
|
8000abc2be | ||
|
|
62a05e2765 | ||
|
|
f04bff8e91 | ||
|
|
84593502a3 | ||
|
|
d478b404df | ||
|
|
80a9fa68de | ||
|
|
8eb9fc71b8 | ||
|
|
6b1567ddae | ||
|
|
265e931451 | ||
|
|
2a886fb26a | ||
|
|
2235823af3 | ||
|
|
f99162b8ee | ||
|
|
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 | ||
|
|
ff393a3f65 | ||
|
|
f5faf52469 | ||
|
|
b5b4d4b971 | ||
|
|
873299701b | ||
|
|
d14cca30d7 | ||
|
|
5af61b5900 | ||
|
|
a10759c7e9 | ||
|
|
ac2d92007e | ||
|
|
09cfdc3fa2 | ||
|
|
04f26dbff7 | ||
|
|
159f5df8cc | ||
|
|
b823ad8e65 | ||
|
|
cc8bffc272 | ||
|
|
e0b93a03fd | ||
|
|
f7f5837d49 | ||
|
|
c3ee8b3c90 | ||
|
|
4de78e3bab | ||
|
|
426538c8af | ||
|
|
c82404c75b | ||
|
|
9bee9841c1 | ||
|
|
010959d915 | ||
|
|
a600728916 | ||
|
|
bbfb8c7cc2 | ||
|
|
32418ea521 | ||
|
|
2c5c99e9b7 | ||
|
|
a5e5a63e45 | ||
|
|
31b44d2c2e | ||
|
|
da8e8a12de | ||
|
|
6506c97ce1 | ||
|
|
5303a1992c | ||
|
|
042308c319 | ||
|
|
2e97e09f44 | ||
|
|
ccfb9c0dad | ||
|
|
b655d97e9e | ||
|
|
3afcb91db6 | ||
|
|
704e2d6176 | ||
|
|
8314c37b1d | ||
|
|
c2c3dfe917 | ||
|
|
c58a9b3f2c | ||
|
|
65a532a7fd | ||
|
|
704d920dab | ||
|
|
025cb0788f | ||
|
|
82c21d8bb1 | ||
|
|
96f973c961 | ||
|
|
a1ed440945 | ||
|
|
8caa839d99 | ||
|
|
9228e5dea0 | ||
|
|
371ac0921d | ||
|
|
937557e214 | ||
|
|
7fdaf41325 | ||
|
|
577eb4f4ca | ||
|
|
311f41b306 | ||
|
|
78f3b1f403 | ||
|
|
4dc02dcb80 | ||
|
|
2f649e413d | ||
|
|
107ddd3826 | ||
|
|
dfdd2cba99 | ||
|
|
c57d68c3dd | ||
|
|
6cc02b734e | ||
|
|
c5fa09dd86 | ||
|
|
29d59315b2 | ||
|
|
981a3c2db3 | ||
|
|
3f2ea56bf9 | ||
|
|
1679ed1327 | ||
|
|
69a1c1b21b | ||
|
|
5bd51832a0 | ||
|
|
52a69b662d | ||
|
|
7e34d89069 | ||
|
|
b0024b28a5 | ||
|
|
ae5450f75d | ||
|
|
1d1aca1a04 | ||
|
|
3a55316ada | ||
|
|
9ef7c2a0b4 | ||
|
|
e759f3fd0b | ||
|
|
03429db877 | ||
|
|
bb5f421e38 | ||
|
|
7dd3ed815a | ||
|
|
cc56482819 | ||
|
|
40f41847fd | ||
|
|
8485fc8c75 | ||
|
|
f3026df65d | ||
|
|
cfd25e974f | ||
|
|
c52f9c5ec4 | ||
|
|
b91517afd5 | ||
|
|
ee8aaadb29 | ||
|
|
0694f2fa76 | ||
|
|
2c81f3be0f | ||
|
|
8fb2f64e98 | ||
|
|
efd2b80e10 | ||
|
|
a9bbe06966 | ||
|
|
4c6f80b308 | ||
|
|
c8299f7e57 | ||
|
|
445babbca8 | ||
|
|
e5137d13e9 | ||
|
|
fb8f8f4dd3 | ||
|
|
2b8ca4746a | ||
|
|
9231a0e526 | ||
|
|
9fa75f0539 | ||
|
|
76b5568129 | ||
|
|
27efe506a7 | ||
|
|
d9be54575a | ||
|
|
a825b96518 | ||
|
|
221b7a4300 | ||
|
|
1ac784e323 | ||
|
|
aae34f4c43 | ||
|
|
7219648fea | ||
|
|
b7be80744c | ||
|
|
29ca18d3f3 | ||
|
|
d9704a999d | ||
|
|
a23983032a | ||
|
|
99d68cfd91 | ||
|
|
9c009a84f2 | ||
|
|
e8ca64fabc |
933 changed files with 27324 additions and 26740 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
183
CONTRIBUTING.md
183
CONTRIBUTING.md
|
|
@ -1,13 +1,186 @@
|
||||||
|
|
||||||
# How to Contribute
|
# How to Contribute
|
||||||
|
|
||||||
We're always looking for people to help make Radarr even better, there are a number of ways to contribute.
|
We're always looking for people to help make Radarr even better, there are a number of ways to contribute.
|
||||||
|
|
||||||
This file has been moved to the wiki for the latest details please see the [contributing wiki page](https://wiki.servarr.com/radarr/contributing).
|
# Documentation
|
||||||
|
|
||||||
## Documentation
|
Setup guides, [FAQ](/radarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/radarr) the better.
|
||||||
|
|
||||||
Setup guides, [FAQ](https://wiki.servarr.com/radarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/radarr) the better.
|
# Development
|
||||||
|
|
||||||
## Development
|
Radarr is written in C# (backend) and JS (frontend). The backend is built on the .NET6 (and _soon_ .NET8) framework, while the frontend utilizes Reactjs.
|
||||||
|
|
||||||
See the [Wiki Page](https://wiki.servarr.com/radarr/contributing)
|
## Tools required
|
||||||
|
|
||||||
|
- Visual Studio 2022 or higher is recommended (<https://www.visualstudio.com/vs/>). The community version is free and works (<https://www.visualstudio.com/downloads/>).
|
||||||
|
|
||||||
|
> VS 2022 V17.0 or higher is recommended as it includes the .NET6 SDK
|
||||||
|
{.is-info}
|
||||||
|
|
||||||
|
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
|
||||||
|
- [Git](https://git-scm.com/downloads)
|
||||||
|
- The [Node.js](https://nodejs.org/) runtime is required. The following versions are supported:
|
||||||
|
- **20** (any minor or patch version within this)
|
||||||
|
{.grid-list}
|
||||||
|
|
||||||
|
> The Application will **NOT** run on older versions such as `18.x`, `16.x` or any version below 20.0! Due to a dependency issue, it will also not run on `21.x` and is untested on other verisons.
|
||||||
|
{.is-warning}
|
||||||
|
|
||||||
|
- [Yarn](https://yarnpkg.com/getting-started/install) is required to build the frontend
|
||||||
|
- Yarn is included with **Node 20**+ by default. Enable it with `corepack enable`
|
||||||
|
- For other Node versions, install it with `npm i -g corepack`
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
1. Fork Radarr
|
||||||
|
1. Clone the repository into your development machine. [*info*](https://docs.github.com/en/get-started/quickstart/fork-a-repo)
|
||||||
|
|
||||||
|
> Be sure to run lint `yarn lint --fix` on your code for any front end changes before committing.
|
||||||
|
For css changes `yarn stylelint-windows --fix` {.is-info}
|
||||||
|
|
||||||
|
### Building the frontend
|
||||||
|
|
||||||
|
- Navigate to the cloned directory
|
||||||
|
- Install the required Node Packages
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
- Start webpack to monitor your development environment for any changes that need post processing using:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Building the Backend
|
||||||
|
|
||||||
|
The backend solution is most easily built and ran in Visual Studio or Rider, however if the only priority is working on the frontend UI it can be built easily from command line as well when the correct SDK is installed.
|
||||||
|
|
||||||
|
#### Visual Studio
|
||||||
|
|
||||||
|
> Ensure startup project is set to `Radarr.Console` and framework to `net6.0`
|
||||||
|
{.is-info}
|
||||||
|
|
||||||
|
1. First `Build` the solution in Visual Studio, this will ensure all projects are correctly built and dependencies restored
|
||||||
|
1. Next `Debug/Run` the project in Visual Studio to start Radarr
|
||||||
|
1. Open <http://localhost:7878>
|
||||||
|
|
||||||
|
#### Command line
|
||||||
|
|
||||||
|
1. Clean solution
|
||||||
|
|
||||||
|
```shell
|
||||||
|
dotnet clean src/Radarr.sln -c Debug
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Restore and Build debug configuration for the correct platform (Posix or Windows)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
dotnet msbuild -restore src/Radarr.sln -p:Configuration=Debug -p:Platform=Posix -t:PublishAllRids
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Run the produced executable from `/_output`
|
||||||
|
|
||||||
|
## Contributing Code
|
||||||
|
|
||||||
|
- If you're adding a new, already requested feature, please comment on [GitHub Issues](https://github.com/Radarr/Radarr/issues) so work is not duplicated (If you want to add something not already on there, please talk to us first)
|
||||||
|
- Rebase from Radarr's develop branch, do not merge
|
||||||
|
- Make meaningful commits, or squash them
|
||||||
|
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
|
||||||
|
- Reach out to us on the discord if you have any questions
|
||||||
|
- Add tests (unit/integration)
|
||||||
|
- Commit with \*nix line endings for consistency (We checkout Windows and commit \*nix)
|
||||||
|
- One feature/bug fix per pull request to keep things clean and easy to understand
|
||||||
|
- Use 4 spaces instead of tabs, this is the default for VS 2022 and WebStorm
|
||||||
|
|
||||||
|
## Pull Requesting
|
||||||
|
|
||||||
|
- Only make pull requests to `develop`, never `master`, if you make a PR to `master` we will comment on it and close it
|
||||||
|
- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability
|
||||||
|
- We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it
|
||||||
|
- Each PR should come from its own [feature branch](http://martinfowler.com/bliki/FeatureBranch.html) not develop in your fork, it should have a meaningful branch name (what is being added/fixed)
|
||||||
|
- `new-feature` (Good)
|
||||||
|
- `fix-bug` (Good)
|
||||||
|
- `patch` (Bad)
|
||||||
|
- `develop` (Bad)
|
||||||
|
- Commits should be wrote as `New:` or `Fixed:` for changes that would not be considered a `maintenance release`
|
||||||
|
|
||||||
|
## Unit Testing
|
||||||
|
|
||||||
|
Radarr utilizes nunit for its unit, integration, and automation test suite.
|
||||||
|
|
||||||
|
### Running Tests
|
||||||
|
|
||||||
|
Tests can be run easily from within VS using the included nunit3testadapter nuget package or from the command line using the included bash script `test.sh`.
|
||||||
|
|
||||||
|
From VS simply navigate to Test Explorer and run or debug the tests you'd like to examine.
|
||||||
|
|
||||||
|
Tests can be run all at once or one at a time in VS.
|
||||||
|
|
||||||
|
From command line the `test.sh` script accepts 3 parameters
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test.sh <PLATFORM> <TYPE> <COVERAGE>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Writing Tests
|
||||||
|
|
||||||
|
While not always fun, we encourage writing unit tests for any backend code changes. This will ensure the change is functioning as you intended and that future changes dont break the expected behavior.
|
||||||
|
|
||||||
|
> We currently require 80% coverage on new code when submitting a PR
|
||||||
|
{.is-info}
|
||||||
|
|
||||||
|
If you have any questions about any of this, please let us know.
|
||||||
|
|
||||||
|
# Translation
|
||||||
|
|
||||||
|
Radarr uses a self hosted open access [Weblate](https://translate.servarr.com) instance to manage its json translation files. These files are stored in the repo at `src/NzbDrone.Core/Localization`
|
||||||
|
|
||||||
|
## Contributing to an Existing Translation
|
||||||
|
|
||||||
|
Weblate handles synchronization and translation of strings for all languages other than English. Editing of translated strings and translating existing strings for supported languages should be performed there for the Radarr project.
|
||||||
|
|
||||||
|
The English translation, `en.json`, serves as the source for all other translations and is managed on GitHub repo.
|
||||||
|
|
||||||
|
## Adding a Language
|
||||||
|
|
||||||
|
Adding translations to Radarr requires two steps
|
||||||
|
|
||||||
|
- Adding the Language to weblate
|
||||||
|
- Adding the Language to Radarr codebase
|
||||||
|
|
||||||
|
## Adding Translation Strings in Code
|
||||||
|
|
||||||
|
The English translation, `src/NzbDrone.Core/Localization/en.json`, serves as the source for all other translations and is managed on GitHub repo. When adding a new string to either the UI or backend a key must also be added to `en.json` along with the default value in English. This key may then be consumed as follows:
|
||||||
|
|
||||||
|
> PRs for translation of log messages will not be accepted
|
||||||
|
{.is-warning}
|
||||||
|
|
||||||
|
### Backend Strings
|
||||||
|
|
||||||
|
Backend strings may be added utilizing the Localization Service `GetLocalizedString` method
|
||||||
|
|
||||||
|
```dotnet
|
||||||
|
private readonly ILocalizationService _localizationService;
|
||||||
|
|
||||||
|
public IndexerCheck(ILocalizationService localizationService)
|
||||||
|
{
|
||||||
|
_localizationService = localizationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
var translated = _localizationService.GetLocalizedString("IndexerHealthCheckNoIndexers")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend Strings
|
||||||
|
|
||||||
|
New strings can be added to the frontend by importing the translate function and using a key specified from `en.json`
|
||||||
|
|
||||||
|
```js
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{translate('UnableToAddANewIndexerPleaseTryAgain')}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,18 @@ 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.22.1'
|
majorVersion: '6.1.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-2025'
|
||||||
linuxImage: 'ubuntu-22.04'
|
linuxImage: 'ubuntu-24.04'
|
||||||
macImage: 'macOS-13'
|
macImage: 'macOS-15'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
|
|
@ -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
|
||||||
|
|
@ -481,6 +469,7 @@ stages:
|
||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Unit Tests'
|
testRunTitle: '$(testName) Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: ne(variables['testName'], 'freebsd-x64')
|
||||||
|
|
||||||
- job: Unit_Docker
|
- job: Unit_Docker
|
||||||
displayName: Unit Docker
|
displayName: Unit Docker
|
||||||
|
|
@ -492,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
|
||||||
|
|
@ -540,7 +519,8 @@ stages:
|
||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Unit Tests'
|
testRunTitle: '$(testName) Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
|
|
||||||
- job: Unit_LinuxCore_Postgres14
|
- job: Unit_LinuxCore_Postgres14
|
||||||
displayName: Unit Native LinuxCore with Postgres14 Database
|
displayName: Unit Native LinuxCore with Postgres14 Database
|
||||||
dependsOn: Prepare
|
dependsOn: Prepare
|
||||||
|
|
@ -557,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'
|
||||||
|
|
@ -596,6 +576,7 @@ stages:
|
||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
|
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
|
|
||||||
- job: Unit_LinuxCore_Postgres15
|
- job: Unit_LinuxCore_Postgres15
|
||||||
displayName: Unit Native LinuxCore with Postgres15 Database
|
displayName: Unit Native LinuxCore with Postgres15 Database
|
||||||
|
|
@ -608,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'
|
||||||
|
|
@ -652,6 +633,7 @@ stages:
|
||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
|
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
|
|
||||||
- stage: Integration
|
- stage: Integration
|
||||||
displayName: Integration
|
displayName: Integration
|
||||||
|
|
@ -695,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'
|
||||||
|
|
@ -717,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: |
|
||||||
|
|
@ -734,6 +716,7 @@ stages:
|
||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Integration Tests'
|
testRunTitle: '$(testName) Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- job: Integration_LinuxCore_Postgres14
|
- job: Integration_LinuxCore_Postgres14
|
||||||
|
|
@ -771,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: |
|
||||||
|
|
@ -796,6 +779,7 @@ stages:
|
||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
|
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -834,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: |
|
||||||
|
|
@ -859,6 +843,7 @@ stages:
|
||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
|
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- job: Integration_FreeBSD
|
- job: Integration_FreeBSD
|
||||||
|
|
@ -905,6 +890,7 @@ stages:
|
||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'FreeBSD Integration Tests'
|
testRunTitle: 'FreeBSD Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: false
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- job: Integration_Docker
|
- job: Integration_Docker
|
||||||
|
|
@ -918,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
|
||||||
|
|
@ -957,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: |
|
||||||
|
|
@ -974,12 +949,13 @@ stages:
|
||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Integration Tests'
|
testRunTitle: '$(testName) Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- stage: Automation
|
- stage: Automation
|
||||||
displayName: Automation
|
displayName: Automation
|
||||||
dependsOn: Packages
|
dependsOn: Packages
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- job: Automation
|
- job: Automation
|
||||||
strategy:
|
strategy:
|
||||||
|
|
@ -1005,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'
|
||||||
|
|
@ -1027,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: |
|
||||||
|
|
@ -1055,6 +1031,7 @@ stages:
|
||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(osName) Automation Tests'
|
testRunTitle: '$(osName) Automation Tests'
|
||||||
failTaskOnFailedTests: $(failBuild)
|
failTaskOnFailedTests: $(failBuild)
|
||||||
|
failTaskOnMissingResultsFile: $(failBuild)
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- stage: Analyze
|
- stage: Analyze
|
||||||
|
|
@ -1151,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: |
|
||||||
|
|
@ -1217,22 +1194,23 @@ stages:
|
||||||
extraProperties: |
|
extraProperties: |
|
||||||
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
|
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
|
||||||
sonar.coverage.exclusions=**/Radarr.Api.V3/**/*
|
sonar.coverage.exclusions=**/Radarr.Api.V3/**/*
|
||||||
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
|
sonar.cs.cobertura.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.cobertura.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.cobertura.xml'
|
||||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
||||||
publishCodeCoverageResults: true
|
publishCodeCoverageResults: true
|
||||||
|
sourcedirs: src
|
||||||
|
|
||||||
- stage: Report_Out
|
- stage: Report_Out
|
||||||
dependsOn:
|
dependsOn:
|
||||||
|
|
@ -1264,4 +1242,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
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
input1 = """Prometheus.Special.Edition.Fan Edit.2012..BRRip.x264.AAC-m2g
|
|
||||||
Star Wars Episode IV - A New Hope (Despecialized) 1999.mkv
|
|
||||||
Prometheus.(Special.Edition.Remastered).2012.[Bluray-1080p].mkv
|
|
||||||
Prometheus Extended 2012
|
|
||||||
Prometheus Extended Directors Cut Fan Edit 2012
|
|
||||||
Prometheus Director's Cut 2012
|
|
||||||
Prometheus Directors Cut 2012
|
|
||||||
Prometheus.(Extended.Theatrical.Version.IMAX).BluRay.1080p.2012.asdf
|
|
||||||
2001 A Space Odyssey Director's Cut (1968).mkv
|
|
||||||
2001: A Space Odyssey (Extended Directors Cut FanEdit) Bluray 1080p 1968
|
|
||||||
A Fake Movie 2035 Directors 2012.mkv
|
|
||||||
Blade Runner Director's Cut 2049.mkv
|
|
||||||
Prometheus 50th Anniversary Edition 2012.mkv
|
|
||||||
Movie 2in1 2012.mkv
|
|
||||||
Movie IMAX 2012.mkv"""
|
|
||||||
|
|
||||||
output1 = """Special.Edition.Fan Edit BRRip.x264.AAC-m2g
|
|
||||||
Despecialized mkv
|
|
||||||
Special.Edition.Remastered Bluray-1080p].mkv
|
|
||||||
Extended mkv
|
|
||||||
Extended Directors Cut Fan Edit mkv
|
|
||||||
Director's Cut mkv
|
|
||||||
Directors Cut mkv
|
|
||||||
Extended.Theatrical.Version.IMAX asdf
|
|
||||||
Director's Cut mkv
|
|
||||||
Extended Directors Cut FanEdit mkv
|
|
||||||
Directors mkv
|
|
||||||
Director's Cut mkv
|
|
||||||
50th Anniversary Edition mkv
|
|
||||||
2in1 mkv
|
|
||||||
IMAX mkv"""
|
|
||||||
|
|
||||||
inputs = input1.split("\n")
|
|
||||||
outputs = output1.split("\n")
|
|
||||||
real_o = []
|
|
||||||
for output in outputs:
|
|
||||||
real_o.append(output.split(" ")[0].replace(".", " ").strip())
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
|
|
||||||
for inp in inputs:
|
|
||||||
o = real_o[count]
|
|
||||||
print "[TestCase(\"{0}\", \"{1}\")]".format(inp, o)
|
|
||||||
count += 1
|
|
||||||
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 &
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ module.exports = (env) => {
|
||||||
const srcFolder = path.join(frontendFolder, 'src');
|
const srcFolder = path.join(frontendFolder, 'src');
|
||||||
const isProduction = !!env.production;
|
const isProduction = !!env.production;
|
||||||
const isProfiling = isProduction && !!env.profile;
|
const isProfiling = isProduction && !!env.profile;
|
||||||
const inlineWebWorkers = 'no-fallback';
|
|
||||||
|
|
||||||
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
||||||
|
|
||||||
|
|
@ -160,16 +159,6 @@ module.exports = (env) => {
|
||||||
|
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
|
||||||
test: /\.worker\.js$/,
|
|
||||||
use: {
|
|
||||||
loader: 'worker-loader',
|
|
||||||
options: {
|
|
||||||
filename: '[name].js',
|
|
||||||
inline: inlineWebWorkers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
test: [/\.jsx?$/, /\.tsx?$/],
|
test: [/\.jsx?$/, /\.tsx?$/],
|
||||||
exclude: /(node_modules|JsLibraries)/,
|
exclude: /(node_modules|JsLibraries)/,
|
||||||
|
|
@ -187,7 +176,7 @@ module.exports = (env) => {
|
||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: '3.39'
|
corejs: '3.42'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,7 @@ function Blocklist() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFilterSelect = useCallback(
|
const handleFilterSelect = useCallback(
|
||||||
(selectedFilterKey: string) => {
|
(selectedFilterKey: string | number) => {
|
||||||
dispatch(setBlocklistFilter({ selectedFilterKey }));
|
dispatch(setBlocklistFilter({ selectedFilterKey }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@ function History() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFilterSelect = useCallback(
|
const handleFilterSelect = useCallback(
|
||||||
(selectedFilterKey: string) => {
|
(selectedFilterKey: string | number) => {
|
||||||
dispatch(setHistoryFilter({ selectedFilterKey }));
|
dispatch(setHistoryFilter({ selectedFilterKey }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ function Queue() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleFilterSelect = useCallback(
|
const handleFilterSelect = useCallback(
|
||||||
(selectedFilterKey: string) => {
|
(selectedFilterKey: string | number) => {
|
||||||
dispatch(setQueueFilter({ selectedFilterKey }));
|
dispatch(setQueueFilter({ selectedFilterKey }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes } from 'Helpers/Props';
|
||||||
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
|
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
|
||||||
import { CheckInputChanged } from 'typings/inputs';
|
import { InputChanged } from 'typings/inputs';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
function QueueOptions() {
|
function QueueOptions() {
|
||||||
|
|
@ -16,7 +16,7 @@ function QueueOptions() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOptionChange = useCallback(
|
const handleOptionChange = useCallback(
|
||||||
({ name, value }: CheckInputChanged) => {
|
({ name, value }: InputChanged<boolean>) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
setQueueOption({
|
setQueueOption({
|
||||||
[name]: value,
|
[name]: value,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon, { IconProps } from 'Components/Icon';
|
import Icon, { IconKind } from 'Components/Icon';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import { TooltipPosition } from 'Helpers/Props/tooltipPositions';
|
import { TooltipPosition } from 'Helpers/Props/tooltipPositions';
|
||||||
|
|
@ -61,7 +61,7 @@ function QueueStatus(props: QueueStatusProps) {
|
||||||
|
|
||||||
// status === 'downloading'
|
// status === 'downloading'
|
||||||
let iconName = icons.DOWNLOADING;
|
let iconName = icons.DOWNLOADING;
|
||||||
let iconKind: IconProps['kind'] = kinds.DEFAULT;
|
let iconKind: IconKind = kinds.DEFAULT;
|
||||||
let title = translate('Downloading');
|
let title = translate('Downloading');
|
||||||
|
|
||||||
if (status === 'paused') {
|
if (status === 'paused') {
|
||||||
|
|
@ -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') {
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,9 @@ class AddNewMovieModalContent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
<div className={styles.overview}>
|
{overview ? (
|
||||||
{overview}
|
<div className={styles.overview}>{overview}</div>
|
||||||
</div>
|
) : null}
|
||||||
|
|
||||||
<Form>
|
<Form>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
|
|
@ -98,7 +98,9 @@ class AddNewMovieModalContent extends Component {
|
||||||
movieFolder: folder,
|
movieFolder: folder,
|
||||||
isWindows
|
isWindows
|
||||||
}}
|
}}
|
||||||
helpText={translate('SubfolderWillBeCreatedAutomaticallyInterp', [folder])}
|
helpText={translate('AddNewMovieRootFolderHelpText', {
|
||||||
|
folder
|
||||||
|
})}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...rootFolderPath}
|
{...rootFolderPath}
|
||||||
/>
|
/>
|
||||||
|
|
@ -110,7 +112,7 @@ class AddNewMovieModalContent extends Component {
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.MOVIE_MONITORED_SELECT}
|
type={inputTypes.MONITOR_MOVIES_SELECT}
|
||||||
name="monitor"
|
name="monitor"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...monitor}
|
{...monitor}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,10 @@
|
||||||
.inputContainer {
|
.inputContainer {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
|
|
||||||
div {
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
margin-bottom: 3px;
|
margin-bottom: 10px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ class ImportMovieFooter extends Component {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.MOVIE_MONITORED_SELECT}
|
type={inputTypes.MONITOR_MOVIES_SELECT}
|
||||||
name="monitor"
|
name="monitor"
|
||||||
value={monitor}
|
value={monitor}
|
||||||
isDisabled={!selectedCount}
|
isDisabled={!selectedCount}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ function ImportMovieRow(props) {
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.monitor}>
|
<VirtualTableRowCell className={styles.monitor}>
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.MOVIE_MONITORED_SELECT}
|
type={inputTypes.MONITOR_MOVIES_SELECT}
|
||||||
name="monitor"
|
name="monitor"
|
||||||
value={monitor}
|
value={monitor}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
|
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DocumentTitle from 'react-document-title';
|
import DocumentTitle from 'react-document-title';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
import { Store } from 'redux';
|
import { Store } from 'redux';
|
||||||
import PageConnector from 'Components/Page/PageConnector';
|
import Page from 'Components/Page/Page';
|
||||||
import ApplyTheme from './ApplyTheme';
|
import ApplyTheme from './ApplyTheme';
|
||||||
import AppRoutes from './AppRoutes';
|
import AppRoutes from './AppRoutes';
|
||||||
|
|
||||||
|
|
@ -12,17 +13,21 @@ interface AppProps {
|
||||||
history: ConnectedRouterProps['history'];
|
history: ConnectedRouterProps['history'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
function App({ store, history }: AppProps) {
|
function App({ store, history }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<DocumentTitle title={window.Radarr.instanceName}>
|
<DocumentTitle title={window.Radarr.instanceName}>
|
||||||
<Provider store={store}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ConnectedRouter history={history}>
|
<Provider store={store}>
|
||||||
<ApplyTheme />
|
<ConnectedRouter history={history}>
|
||||||
<PageConnector>
|
<ApplyTheme />
|
||||||
<AppRoutes />
|
<Page>
|
||||||
</PageConnector>
|
<AppRoutes />
|
||||||
</ConnectedRouter>
|
</Page>
|
||||||
</Provider>
|
</ConnectedRouter>
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>
|
||||||
</DocumentTitle>
|
</DocumentTitle>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -32,8 +32,8 @@ import Status from 'System/Status/Status';
|
||||||
import Tasks from 'System/Tasks/Tasks';
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
import Updates from 'System/Updates/Updates';
|
import Updates from 'System/Updates/Updates';
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
import CutoffUnmet from 'Wanted/CutoffUnmet/CutoffUnmet';
|
||||||
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
import Missing from 'Wanted/Missing/Missing';
|
||||||
|
|
||||||
function RedirectWithUrlBase() {
|
function RedirectWithUrlBase() {
|
||||||
return <Redirect to={getPathWithUrlBase('/')} />;
|
return <Redirect to={getPathWithUrlBase('/')} />;
|
||||||
|
|
@ -73,7 +73,7 @@ function AppRoutes() {
|
||||||
Calendar
|
Calendar
|
||||||
*/}
|
*/}
|
||||||
|
|
||||||
<Route path="/calendar" component={CalendarPageConnector} />
|
<Route path="/calendar" component={CalendarPage} />
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
Activity
|
Activity
|
||||||
|
|
@ -89,9 +89,9 @@ function AppRoutes() {
|
||||||
Wanted
|
Wanted
|
||||||
*/}
|
*/}
|
||||||
|
|
||||||
<Route path="/wanted/missing" component={MissingConnector} />
|
<Route path="/wanted/missing" component={Missing} />
|
||||||
|
|
||||||
<Route path="/wanted/cutoffunmet" component={CutoffUnmetConnector} />
|
<Route path="/wanted/cutoffunmet" component={CutoffUnmet} />
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
Settings
|
Settings
|
||||||
|
|
@ -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} />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,13 @@ export type SelectContextAction =
|
||||||
| { type: 'unselectAll' }
|
| { type: 'unselectAll' }
|
||||||
| {
|
| {
|
||||||
type: 'toggleSelected';
|
type: 'toggleSelected';
|
||||||
id: number;
|
id: number | string;
|
||||||
isSelected: boolean;
|
isSelected: boolean | null;
|
||||||
shiftKey: boolean;
|
shiftKey: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'removeItem';
|
type: 'removeItem';
|
||||||
id: number;
|
id: number | string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'updateItems';
|
type: 'updateItems';
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
import { ValidationFailure } from 'typings/pending';
|
import { ValidationFailure } from 'typings/pending';
|
||||||
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
import { Filter, FilterBuilderProp } from './AppState';
|
||||||
|
|
||||||
export interface Error {
|
export interface Error {
|
||||||
status?: number;
|
status?: number;
|
||||||
|
|
@ -35,7 +35,7 @@ export interface TableAppSectionState {
|
||||||
|
|
||||||
export interface AppSectionFilterState<T> {
|
export interface AppSectionFilterState<T> {
|
||||||
selectedFilterKey: string;
|
selectedFilterKey: string;
|
||||||
filters: PropertyFilter[];
|
filters: Filter[];
|
||||||
filterBuilderProps: FilterBuilderProp<T>[];
|
filterBuilderProps: FilterBuilderProp<T>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,30 @@
|
||||||
|
import { Error } from './AppSectionState';
|
||||||
import BlocklistAppState from './BlocklistAppState';
|
import BlocklistAppState from './BlocklistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
|
import CaptchaAppState from './CaptchaAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
|
import CustomFiltersAppState from './CustomFiltersAppState';
|
||||||
import ExtraFilesAppState from './ExtraFilesAppState';
|
import ExtraFilesAppState from './ExtraFilesAppState';
|
||||||
import HistoryAppState, { MovieHistoryAppState } from './HistoryAppState';
|
import HistoryAppState, { MovieHistoryAppState } from './HistoryAppState';
|
||||||
import InteractiveImportAppState from './InteractiveImportAppState';
|
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||||
|
import MessagesAppState from './MessagesAppState';
|
||||||
import MovieBlocklistAppState from './MovieBlocklistAppState';
|
import MovieBlocklistAppState from './MovieBlocklistAppState';
|
||||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||||
import MovieCreditAppState from './MovieCreditAppState';
|
import MovieCreditAppState from './MovieCreditAppState';
|
||||||
import MovieFilesAppState from './MovieFilesAppState';
|
import MovieFilesAppState from './MovieFilesAppState';
|
||||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||||
|
import OAuthAppState from './OAuthAppState';
|
||||||
import OrganizePreviewAppState from './OrganizePreviewAppState';
|
import OrganizePreviewAppState from './OrganizePreviewAppState';
|
||||||
import ParseAppState from './ParseAppState';
|
import ParseAppState from './ParseAppState';
|
||||||
import PathsAppState from './PathsAppState';
|
import PathsAppState from './PathsAppState';
|
||||||
|
import ProviderOptionsAppState from './ProviderOptionsAppState';
|
||||||
import QueueAppState from './QueueAppState';
|
import QueueAppState from './QueueAppState';
|
||||||
import ReleasesAppState from './ReleasesAppState';
|
import ReleasesAppState from './ReleasesAppState';
|
||||||
import RootFolderAppState from './RootFolderAppState';
|
import RootFolderAppState from './RootFolderAppState';
|
||||||
import SettingsAppState from './SettingsAppState';
|
import SettingsAppState from './SettingsAppState';
|
||||||
import SystemAppState from './SystemAppState';
|
import SystemAppState from './SystemAppState';
|
||||||
import TagsAppState from './TagsAppState';
|
import TagsAppState from './TagsAppState';
|
||||||
|
import WantedAppState from './WantedAppState';
|
||||||
|
|
||||||
interface FilterBuilderPropOption {
|
interface FilterBuilderPropOption {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -40,35 +47,45 @@ export interface PropertyFilter {
|
||||||
|
|
||||||
export interface Filter {
|
export interface Filter {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string | (() => string);
|
||||||
filers: PropertyFilter[];
|
filters: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomFilter {
|
export interface CustomFilter {
|
||||||
id: number;
|
id: number;
|
||||||
type: string;
|
type: string;
|
||||||
label: string;
|
label: string;
|
||||||
filers: PropertyFilter[];
|
filters: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionState {
|
export interface AppSectionState {
|
||||||
|
isUpdated: boolean;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
|
isDisconnected: boolean;
|
||||||
isReconnecting: boolean;
|
isReconnecting: boolean;
|
||||||
isSidebarVisible: boolean;
|
isSidebarVisible: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
prevVersion?: string;
|
prevVersion?: string;
|
||||||
dimensions: {
|
dimensions: {
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
|
isLargeScreen: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
|
translations: {
|
||||||
|
error?: Error;
|
||||||
|
isPopulated: boolean;
|
||||||
|
};
|
||||||
|
messages: MessagesAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
app: AppSectionState;
|
app: AppSectionState;
|
||||||
blocklist: BlocklistAppState;
|
blocklist: BlocklistAppState;
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
|
captcha: CaptchaAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
|
customFilters: CustomFiltersAppState;
|
||||||
extraFiles: ExtraFilesAppState;
|
extraFiles: ExtraFilesAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
|
|
@ -79,15 +96,18 @@ interface AppState {
|
||||||
movieHistory: MovieHistoryAppState;
|
movieHistory: MovieHistoryAppState;
|
||||||
movieIndex: MovieIndexAppState;
|
movieIndex: MovieIndexAppState;
|
||||||
movies: MoviesAppState;
|
movies: MoviesAppState;
|
||||||
|
oAuth: OAuthAppState;
|
||||||
organizePreview: OrganizePreviewAppState;
|
organizePreview: OrganizePreviewAppState;
|
||||||
parse: ParseAppState;
|
parse: ParseAppState;
|
||||||
paths: PathsAppState;
|
paths: PathsAppState;
|
||||||
|
providerOptions: ProviderOptionsAppState;
|
||||||
queue: QueueAppState;
|
queue: QueueAppState;
|
||||||
releases: ReleasesAppState;
|
releases: ReleasesAppState;
|
||||||
rootFolders: RootFolderAppState;
|
rootFolders: RootFolderAppState;
|
||||||
settings: SettingsAppState;
|
settings: SettingsAppState;
|
||||||
system: SystemAppState;
|
system: SystemAppState;
|
||||||
tags: TagsAppState;
|
tags: TagsAppState;
|
||||||
|
wanted: WantedAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppState;
|
export default AppState;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
11
frontend/src/App/State/CaptchaAppState.ts
Normal file
11
frontend/src/App/State/CaptchaAppState.ts
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
interface CaptchaAppState {
|
||||||
|
refreshing: false;
|
||||||
|
token: string;
|
||||||
|
siteKey: unknown;
|
||||||
|
secretToken: unknown;
|
||||||
|
ray: unknown;
|
||||||
|
stoken: unknown;
|
||||||
|
responseUrl: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CaptchaAppState;
|
||||||
15
frontend/src/App/State/MessagesAppState.ts
Normal file
15
frontend/src/App/State/MessagesAppState.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
|
||||||
|
export type MessageType = 'error' | 'info' | 'success' | 'warning';
|
||||||
|
|
||||||
|
export interface Message extends ModelBase {
|
||||||
|
hideAfter: number;
|
||||||
|
message: string;
|
||||||
|
name: string;
|
||||||
|
type: MessageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessagesAppState = AppSectionState<Message>;
|
||||||
|
|
||||||
|
export default MessagesAppState;
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
|
Error,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import MovieCollection from 'typings/MovieCollection';
|
import MovieCollection from 'typings/MovieCollection';
|
||||||
|
|
||||||
interface MovieCollectionAppState
|
interface MovieCollectionAppState
|
||||||
extends AppSectionState<MovieCollection>,
|
extends AppSectionState<MovieCollection>,
|
||||||
|
AppSectionFilterState<MovieCollection>,
|
||||||
AppSectionSaveState {
|
AppSectionSaveState {
|
||||||
itemMap: Record<number, number>;
|
itemMap: Record<number, number>;
|
||||||
|
|
||||||
|
isAdding: boolean;
|
||||||
|
addError: Error;
|
||||||
|
|
||||||
|
pendingChanges: Partial<MovieCollection>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MovieCollectionAppState;
|
export default MovieCollectionAppState;
|
||||||
|
|
|
||||||
9
frontend/src/App/State/OAuthAppState.ts
Normal file
9
frontend/src/App/State/OAuthAppState.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Error } from './AppSectionState';
|
||||||
|
|
||||||
|
interface OAuthAppState {
|
||||||
|
authorizing: boolean;
|
||||||
|
result: Record<string, unknown> | null;
|
||||||
|
error: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OAuthAppState;
|
||||||
22
frontend/src/App/State/ProviderOptionsAppState.ts
Normal file
22
frontend/src/App/State/ProviderOptionsAppState.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
import Field, { FieldSelectOption } from 'typings/Field';
|
||||||
|
|
||||||
|
export interface ProviderOptions {
|
||||||
|
fields?: Field[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderOptionsDevice {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderOptionsAppState {
|
||||||
|
devices: AppSectionState<ProviderOptionsDevice>;
|
||||||
|
servers: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
newznabCategories: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getProfiles: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getTags: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getRootFolders: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProviderOptionsAppState;
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import DiskSpace from 'typings/DiskSpace';
|
import DiskSpace from 'typings/DiskSpace';
|
||||||
import Health from 'typings/Health';
|
import Health from 'typings/Health';
|
||||||
|
import LogFile from 'typings/LogFile';
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
import SystemStatus from 'typings/SystemStatus';
|
||||||
import Task from 'typings/Task';
|
import Task from 'typings/Task';
|
||||||
import Update from 'typings/Update';
|
import Update from 'typings/Update';
|
||||||
|
|
@ -9,13 +10,16 @@ export type DiskSpaceAppState = AppSectionState<DiskSpace>;
|
||||||
export type HealthAppState = AppSectionState<Health>;
|
export type HealthAppState = AppSectionState<Health>;
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||||
export type TaskAppState = AppSectionState<Task>;
|
export type TaskAppState = AppSectionState<Task>;
|
||||||
|
export type LogFilesAppState = AppSectionState<LogFile>;
|
||||||
export type UpdateAppState = AppSectionState<Update>;
|
export type UpdateAppState = AppSectionState<Update>;
|
||||||
|
|
||||||
interface SystemAppState {
|
interface SystemAppState {
|
||||||
diskSpace: DiskSpaceAppState;
|
diskSpace: DiskSpaceAppState;
|
||||||
health: HealthAppState;
|
health: HealthAppState;
|
||||||
|
logFiles: LogFilesAppState;
|
||||||
status: SystemStatusAppState;
|
status: SystemStatusAppState;
|
||||||
tasks: TaskAppState;
|
tasks: TaskAppState;
|
||||||
|
updateLogFiles: LogFilesAppState;
|
||||||
updates: UpdateAppState;
|
updates: UpdateAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
29
frontend/src/App/State/WantedAppState.ts
Normal file
29
frontend/src/App/State/WantedAppState.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
import Movie from 'Movie/Movie';
|
||||||
|
|
||||||
|
interface WantedMovie extends Movie {
|
||||||
|
isSaving?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WantedCutoffUnmetAppState
|
||||||
|
extends AppSectionState<WantedMovie>,
|
||||||
|
AppSectionFilterState<WantedMovie>,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState {}
|
||||||
|
|
||||||
|
interface WantedMissingAppState
|
||||||
|
extends AppSectionState<WantedMovie>,
|
||||||
|
AppSectionFilterState<WantedMovie>,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState {}
|
||||||
|
|
||||||
|
interface WantedAppState {
|
||||||
|
cutoffUnmet: WantedCutoffUnmetAppState;
|
||||||
|
missing: WantedMissingAppState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WantedAppState;
|
||||||
|
|
@ -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);
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import AddNewCollectionMovieModalContentConnector from './AddNewCollectionMovieModalContentConnector';
|
|
||||||
|
|
||||||
function AddNewCollectionMovieModal(props) {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
onModalClose,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<AddNewCollectionMovieModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddNewCollectionMovieModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddNewCollectionMovieModal;
|
|
||||||
|
|
@ -1,204 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
|
||||||
import Form from 'Components/Form/Form';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './AddNewCollectionMovieModalContent.css';
|
|
||||||
|
|
||||||
class AddNewCollectionMovieModalContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onQualityProfileIdChange = ({ value }) => {
|
|
||||||
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddMoviePress = () => {
|
|
||||||
this.props.onAddMoviePress();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
year,
|
|
||||||
overview,
|
|
||||||
images,
|
|
||||||
isAdding,
|
|
||||||
folder,
|
|
||||||
tags,
|
|
||||||
isSmallScreen,
|
|
||||||
isWindows,
|
|
||||||
onModalClose,
|
|
||||||
onInputChange,
|
|
||||||
rootFolderPath,
|
|
||||||
monitor,
|
|
||||||
qualityProfileId,
|
|
||||||
minimumAvailability,
|
|
||||||
searchForMovie
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{title}
|
|
||||||
|
|
||||||
{
|
|
||||||
!title.contains(year) && !!year &&
|
|
||||||
<span className={styles.year}>({year})</span>
|
|
||||||
}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div className={styles.container}>
|
|
||||||
{
|
|
||||||
!isSmallScreen &&
|
|
||||||
<div className={styles.poster}>
|
|
||||||
<MoviePoster
|
|
||||||
className={styles.poster}
|
|
||||||
images={images}
|
|
||||||
size={250}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<div className={styles.info}>
|
|
||||||
<div className={styles.overview}>
|
|
||||||
{overview}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Form>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('RootFolder')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
|
||||||
name="rootFolderPath"
|
|
||||||
valueOptions={{
|
|
||||||
movieFolder: folder,
|
|
||||||
isWindows
|
|
||||||
}}
|
|
||||||
selectedValueOptions={{
|
|
||||||
movieFolder: folder,
|
|
||||||
isWindows
|
|
||||||
}}
|
|
||||||
helpText={translate('SubfolderWillBeCreatedAutomaticallyInterp', [folder])}
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...rootFolderPath}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>
|
|
||||||
{translate('Monitor')}
|
|
||||||
</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.MOVIE_MONITORED_SELECT}
|
|
||||||
name="monitor"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...monitor}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.AVAILABILITY_SELECT}
|
|
||||||
name="minimumAvailability"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...minimumAvailability}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
|
||||||
name="qualityProfileId"
|
|
||||||
onChange={this.onQualityProfileIdChange}
|
|
||||||
{...qualityProfileId}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Tags')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.TAG}
|
|
||||||
name="tags"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...tags}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter className={styles.modalFooter}>
|
|
||||||
<label className={styles.searchForMissingMovieLabelContainer}>
|
|
||||||
<span className={styles.searchForMissingMovieLabel}>
|
|
||||||
{translate('StartSearchForMissingMovie')}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<CheckInput
|
|
||||||
containerClassName={styles.searchForMissingMovieContainer}
|
|
||||||
className={styles.searchForMissingMovieInput}
|
|
||||||
name="searchForMovie"
|
|
||||||
onChange={onInputChange}
|
|
||||||
{...searchForMovie}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<SpinnerButton
|
|
||||||
className={styles.addButton}
|
|
||||||
kind={kinds.SUCCESS}
|
|
||||||
isSpinning={isAdding}
|
|
||||||
onPress={this.onAddMoviePress}
|
|
||||||
>
|
|
||||||
{translate('AddMovie')}
|
|
||||||
</SpinnerButton>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddNewCollectionMovieModalContent.propTypes = {
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
year: PropTypes.number.isRequired,
|
|
||||||
overview: PropTypes.string,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isAdding: PropTypes.bool.isRequired,
|
|
||||||
addError: PropTypes.object,
|
|
||||||
rootFolderPath: PropTypes.object,
|
|
||||||
monitor: PropTypes.object.isRequired,
|
|
||||||
qualityProfileId: PropTypes.object,
|
|
||||||
minimumAvailability: PropTypes.object.isRequired,
|
|
||||||
searchForMovie: PropTypes.object.isRequired,
|
|
||||||
folder: PropTypes.string.isRequired,
|
|
||||||
tags: PropTypes.object.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
isWindows: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired,
|
|
||||||
onAddMoviePress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddNewCollectionMovieModalContent;
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { addMovie, setMovieCollectionValue } from 'Store/Actions/movieCollectionActions';
|
|
||||||
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
|
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
|
||||||
import AddNewMovieModalContent from './AddNewCollectionMovieModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieCollections,
|
|
||||||
createCollectionSelector(),
|
|
||||||
createDimensionsSelector(),
|
|
||||||
createSystemStatusSelector(),
|
|
||||||
(discoverMovieState, collection, dimensions, systemStatus) => {
|
|
||||||
const {
|
|
||||||
isAdding,
|
|
||||||
addError,
|
|
||||||
pendingChanges
|
|
||||||
} = discoverMovieState;
|
|
||||||
|
|
||||||
const collectionDefaults = {
|
|
||||||
rootFolderPath: collection.rootFolderPath,
|
|
||||||
monitor: 'movieOnly',
|
|
||||||
qualityProfileId: collection.qualityProfileId,
|
|
||||||
minimumAvailability: collection.minimumAvailability,
|
|
||||||
searchForMovie: collection.searchOnAdd,
|
|
||||||
tags: collection.tags || []
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
settings,
|
|
||||||
validationErrors,
|
|
||||||
validationWarnings
|
|
||||||
} = selectSettings(collectionDefaults, pendingChanges, addError);
|
|
||||||
|
|
||||||
return {
|
|
||||||
isAdding,
|
|
||||||
addError,
|
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
|
||||||
validationErrors,
|
|
||||||
validationWarnings,
|
|
||||||
isWindows: systemStatus.isWindows,
|
|
||||||
...settings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
addMovie,
|
|
||||||
setMovieCollectionValue
|
|
||||||
};
|
|
||||||
|
|
||||||
class AddNewCollectionMovieModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.props.setMovieCollectionValue({ name, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddMoviePress = () => {
|
|
||||||
const {
|
|
||||||
tmdbId,
|
|
||||||
title,
|
|
||||||
rootFolderPath,
|
|
||||||
monitor,
|
|
||||||
qualityProfileId,
|
|
||||||
minimumAvailability,
|
|
||||||
searchForMovie,
|
|
||||||
tags
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.addMovie({
|
|
||||||
tmdbId,
|
|
||||||
title,
|
|
||||||
rootFolderPath: rootFolderPath.value,
|
|
||||||
monitor: monitor.value,
|
|
||||||
qualityProfileId: qualityProfileId.value,
|
|
||||||
minimumAvailability: minimumAvailability.value,
|
|
||||||
searchForMovie: searchForMovie.value,
|
|
||||||
tags: tags.value
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.onModalClose(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<AddNewMovieModalContent
|
|
||||||
{...this.props}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onAddMoviePress={this.onAddMoviePress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AddNewCollectionMovieModalContentConnector.propTypes = {
|
|
||||||
tmdbId: PropTypes.number.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
rootFolderPath: PropTypes.object,
|
|
||||||
monitor: PropTypes.object.isRequired,
|
|
||||||
qualityProfileId: PropTypes.object,
|
|
||||||
minimumAvailability: PropTypes.object.isRequired,
|
|
||||||
searchForMovie: PropTypes.object.isRequired,
|
|
||||||
tags: PropTypes.object.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
addMovie: PropTypes.func.isRequired,
|
|
||||||
setMovieCollectionValue: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewCollectionMovieModalContentConnector);
|
|
||||||
45
frontend/src/Collection/AddNewMovieCollectionMovieModal.tsx
Normal file
45
frontend/src/Collection/AddNewMovieCollectionMovieModal.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import AddNewMovieCollectionMovieModalContent, {
|
||||||
|
AddNewMovieCollectionMovieModalContentProps,
|
||||||
|
} from './AddNewMovieCollectionMovieModalContent';
|
||||||
|
|
||||||
|
interface AddNewCollectionMovieModalProps
|
||||||
|
extends AddNewMovieCollectionMovieModalContentProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddNewMovieCollectionMovieModal({
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
}: AddNewCollectionMovieModalProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const wasOpen = usePrevious(isOpen);
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
dispatch(clearPendingChanges({ section: 'movieCollections' }));
|
||||||
|
onModalClose();
|
||||||
|
}, [dispatch, onModalClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasOpen && !isOpen) {
|
||||||
|
dispatch(clearPendingChanges({ section: 'movieCollections' }));
|
||||||
|
}
|
||||||
|
}, [wasOpen, isOpen, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
|
||||||
|
<AddNewMovieCollectionMovieModalContent
|
||||||
|
{...otherProps}
|
||||||
|
onModalClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddNewMovieCollectionMovieModal;
|
||||||
|
|
@ -0,0 +1,269 @@
|
||||||
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import useMovieCollection from 'Collection/useMovieCollection';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import { Image } from 'Movie/Movie';
|
||||||
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
|
import {
|
||||||
|
addMovie,
|
||||||
|
setMovieCollectionValue,
|
||||||
|
} from 'Store/Actions/movieCollectionActions';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import useIsWindows from 'System/useIsWindows';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './AddNewMovieCollectionMovieModalContent.css';
|
||||||
|
|
||||||
|
export interface AddNewMovieCollectionMovieModalContentProps {
|
||||||
|
tmdbId: number;
|
||||||
|
title: string;
|
||||||
|
year: number;
|
||||||
|
overview?: string;
|
||||||
|
images: Image[];
|
||||||
|
collectionId: number;
|
||||||
|
folder: string;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AddNewMovieCollectionMovieModalContent({
|
||||||
|
tmdbId,
|
||||||
|
title,
|
||||||
|
year,
|
||||||
|
overview,
|
||||||
|
images,
|
||||||
|
collectionId,
|
||||||
|
folder,
|
||||||
|
onModalClose,
|
||||||
|
}: AddNewMovieCollectionMovieModalContentProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const collection = useMovieCollection(collectionId)!;
|
||||||
|
|
||||||
|
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||||
|
const isWindows = useIsWindows();
|
||||||
|
|
||||||
|
const { isAdding, addError, pendingChanges } = useSelector(
|
||||||
|
(state: AppState) => state.movieCollections
|
||||||
|
);
|
||||||
|
|
||||||
|
const wasAdding = usePrevious(isAdding);
|
||||||
|
|
||||||
|
const { settings, validationErrors, validationWarnings } = useMemo(() => {
|
||||||
|
const options = {
|
||||||
|
rootFolderPath: collection.rootFolderPath,
|
||||||
|
monitor: collection.monitored ? 'movieOnly' : 'none',
|
||||||
|
qualityProfileId: collection.qualityProfileId,
|
||||||
|
minimumAvailability: collection.minimumAvailability,
|
||||||
|
searchForMovie: collection.searchOnAdd,
|
||||||
|
tags: collection.tags || [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return selectSettings(options, pendingChanges, addError);
|
||||||
|
}, [collection, pendingChanges, addError]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
minimumAvailability,
|
||||||
|
rootFolderPath,
|
||||||
|
searchForMovie,
|
||||||
|
tags,
|
||||||
|
} = settings;
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
({ name, value }: InputChanged) => {
|
||||||
|
// @ts-expect-error actions aren't typed
|
||||||
|
dispatch(setMovieCollectionValue({ name, value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleAddMoviePress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
addMovie({
|
||||||
|
tmdbId,
|
||||||
|
title,
|
||||||
|
rootFolderPath: rootFolderPath.value,
|
||||||
|
monitor: monitor.value,
|
||||||
|
qualityProfileId: qualityProfileId.value,
|
||||||
|
minimumAvailability: minimumAvailability.value,
|
||||||
|
searchForMovie: searchForMovie.value,
|
||||||
|
tags: tags.value,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
tmdbId,
|
||||||
|
title,
|
||||||
|
rootFolderPath,
|
||||||
|
monitor,
|
||||||
|
qualityProfileId,
|
||||||
|
minimumAvailability,
|
||||||
|
searchForMovie,
|
||||||
|
tags,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isAdding && wasAdding && !addError) {
|
||||||
|
onModalClose();
|
||||||
|
}
|
||||||
|
}, [isAdding, wasAdding, addError, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{title}
|
||||||
|
|
||||||
|
{!title.includes(String(year)) && year ? (
|
||||||
|
<span className={styles.year}>({year})</span>
|
||||||
|
) : null}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div className={styles.container}>
|
||||||
|
{isSmallScreen ? null : (
|
||||||
|
<div className={styles.poster}>
|
||||||
|
<MoviePoster
|
||||||
|
className={styles.poster}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.info}>
|
||||||
|
{overview ? (
|
||||||
|
<div className={styles.overview}>{overview}</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Form
|
||||||
|
validationErrors={validationErrors}
|
||||||
|
validationWarnings={validationWarnings}
|
||||||
|
>
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
|
name="rootFolderPath"
|
||||||
|
valueOptions={{
|
||||||
|
movieFolder: folder,
|
||||||
|
isWindows,
|
||||||
|
}}
|
||||||
|
selectedValueOptions={{
|
||||||
|
movieFolder: folder,
|
||||||
|
isWindows,
|
||||||
|
}}
|
||||||
|
helpText={translate('AddNewMovieRootFolderHelpText', {
|
||||||
|
folder,
|
||||||
|
})}
|
||||||
|
{...rootFolderPath}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Monitor')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.MONITOR_MOVIES_SELECT}
|
||||||
|
name="monitor"
|
||||||
|
{...monitor}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
{translate('MinimumAvailability')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon className={styles.labelIcon} name={icons.INFO} />
|
||||||
|
}
|
||||||
|
title={translate('MinimumAvailability')}
|
||||||
|
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
|
name="minimumAvailability"
|
||||||
|
{...minimumAvailability}
|
||||||
|
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||||
|
name="qualityProfileId"
|
||||||
|
{...qualityProfileId}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Tags')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TAG}
|
||||||
|
name="tags"
|
||||||
|
{...tags}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter className={styles.modalFooter}>
|
||||||
|
<label className={styles.searchForMissingMovieLabelContainer}>
|
||||||
|
<span className={styles.searchForMissingMovieLabel}>
|
||||||
|
{translate('StartSearchForMissingMovie')}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<CheckInput
|
||||||
|
containerClassName={styles.searchForMissingMovieContainer}
|
||||||
|
className={styles.searchForMissingMovieInput}
|
||||||
|
name="searchForMovie"
|
||||||
|
{...searchForMovie}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<SpinnerButton
|
||||||
|
className={styles.addButton}
|
||||||
|
kind={kinds.SUCCESS}
|
||||||
|
isSpinning={isAdding}
|
||||||
|
onPress={handleAddMoviePress}
|
||||||
|
>
|
||||||
|
{translate('AddMovie')}
|
||||||
|
</SpinnerButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddNewMovieCollectionMovieModalContent;
|
||||||
|
|
@ -18,9 +18,9 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
import CollectionFooter from './CollectionFooter';
|
import CollectionFooter from './CollectionFooter';
|
||||||
import CollectionFilterMenu from './Menus/CollectionFilterMenu';
|
import MovieCollectionFilterMenu from './Menus/MovieCollectionFilterMenu';
|
||||||
import CollectionSortMenu from './Menus/CollectionSortMenu';
|
import MovieCollectionSortMenu from './Menus/MovieCollectionSortMenu';
|
||||||
import NoCollection from './NoCollection';
|
import NoMovieCollections from './NoMovieCollections';
|
||||||
import CollectionOverviewsConnector from './Overview/CollectionOverviewsConnector';
|
import CollectionOverviewsConnector from './Overview/CollectionOverviewsConnector';
|
||||||
import CollectionOverviewOptionsModal from './Overview/Options/CollectionOverviewOptionsModal';
|
import CollectionOverviewOptionsModal from './Overview/Options/CollectionOverviewOptionsModal';
|
||||||
|
|
||||||
|
|
@ -224,6 +224,7 @@ class Collection extends Component {
|
||||||
view,
|
view,
|
||||||
onSortSelect,
|
onSortSelect,
|
||||||
onFilterSelect,
|
onFilterSelect,
|
||||||
|
initialScrollTop,
|
||||||
onScroll,
|
onScroll,
|
||||||
isRefreshingCollections,
|
isRefreshingCollections,
|
||||||
isSaving,
|
isSaving,
|
||||||
|
|
@ -247,7 +248,7 @@ class Collection extends Component {
|
||||||
const hasNoCollection = !totalItems;
|
const hasNoCollection = !totalItems;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent>
|
<PageContent title={translate('Collections')}>
|
||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
<PageToolbarSection>
|
<PageToolbarSection>
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
|
|
@ -284,14 +285,14 @@ class Collection extends Component {
|
||||||
<PageToolbarSeparator />
|
<PageToolbarSeparator />
|
||||||
}
|
}
|
||||||
|
|
||||||
<CollectionSortMenu
|
<MovieCollectionSortMenu
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
isDisabled={hasNoCollection}
|
isDisabled={hasNoCollection}
|
||||||
onSortSelect={onSortSelect}
|
onSortSelect={onSortSelect}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CollectionFilterMenu
|
<MovieCollectionFilterMenu
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
customFilters={customFilters}
|
customFilters={customFilters}
|
||||||
|
|
@ -306,6 +307,7 @@ class Collection extends Component {
|
||||||
ref={this.scrollerRef}
|
ref={this.scrollerRef}
|
||||||
className={styles.contentBody}
|
className={styles.contentBody}
|
||||||
innerClassName={styles[`${view}InnerContentBody`]}
|
innerClassName={styles[`${view}InnerContentBody`]}
|
||||||
|
onScroll={onScroll}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
isFetching && !isPopulated &&
|
isFetching && !isPopulated &&
|
||||||
|
|
@ -334,6 +336,7 @@ class Collection extends Component {
|
||||||
onSelectedChange={this.onSelectedChange}
|
onSelectedChange={this.onSelectedChange}
|
||||||
onSelectAllChange={this.onSelectAllChange}
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
selectedState={selectedState}
|
selectedState={selectedState}
|
||||||
|
scrollTop={initialScrollTop}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -341,7 +344,7 @@ class Collection extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
!error && isPopulated && !items.length &&
|
!error && isPopulated && !items.length &&
|
||||||
<NoCollection totalItems={totalItems} />
|
<NoMovieCollections totalItems={totalItems} />
|
||||||
}
|
}
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
|
|
||||||
|
|
@ -374,6 +377,7 @@ class Collection extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
Collection.propTypes = {
|
Collection.propTypes = {
|
||||||
|
initialScrollTop: PropTypes.number,
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
|
|
||||||
|
|
@ -5,14 +5,17 @@ import { createSelector } from 'reselect';
|
||||||
import * as commandNames from 'Commands/commandNames';
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import withScrollPosition from 'Components/withScrollPosition';
|
import withScrollPosition from 'Components/withScrollPosition';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import { saveMovieCollections, setMovieCollectionsFilter, setMovieCollectionsSort } from 'Store/Actions/movieCollectionActions';
|
import {
|
||||||
|
fetchMovieCollections,
|
||||||
|
saveMovieCollections,
|
||||||
|
setMovieCollectionsFilter,
|
||||||
|
setMovieCollectionsSort
|
||||||
|
} from 'Store/Actions/movieCollectionActions';
|
||||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
|
||||||
import scrollPositions from 'Store/scrollPositions';
|
import scrollPositions from 'Store/scrollPositions';
|
||||||
import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector';
|
import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
|
||||||
import Collection from './Collection';
|
import Collection from './Collection';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
|
@ -36,8 +39,8 @@ function createMapStateToProps() {
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
return {
|
return {
|
||||||
dispatchFetchRootFolders() {
|
dispatchFetchMovieCollections() {
|
||||||
dispatch(fetchRootFolders());
|
dispatch(fetchMovieCollections());
|
||||||
},
|
},
|
||||||
dispatchFetchQueueDetails() {
|
dispatchFetchQueueDetails() {
|
||||||
dispatch(fetchQueueDetails());
|
dispatch(fetchQueueDetails());
|
||||||
|
|
@ -68,13 +71,11 @@ class CollectionConnector extends Component {
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
registerPagePopulator(this.repopulate);
|
this.props.dispatchFetchMovieCollections();
|
||||||
this.props.dispatchFetchRootFolders();
|
|
||||||
this.props.dispatchFetchQueueDetails();
|
this.props.dispatchFetchQueueDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
unregisterPagePopulator(this.repopulate);
|
|
||||||
this.props.dispatchClearQueueDetails();
|
this.props.dispatchClearQueueDetails();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,9 +94,16 @@ class CollectionConnector extends Component {
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
dispatchFetchMovieCollections,
|
||||||
|
dispatchFetchQueueDetails,
|
||||||
|
dispatchClearQueueDetails,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collection
|
<Collection
|
||||||
{...this.props}
|
{...otherProps}
|
||||||
onViewSelect={this.onViewSelect}
|
onViewSelect={this.onViewSelect}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
onUpdateSelectedPress={this.onUpdateSelectedPress}
|
onUpdateSelectedPress={this.onUpdateSelectedPress}
|
||||||
|
|
@ -108,7 +116,7 @@ CollectionConnector.propTypes = {
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
view: PropTypes.string.isRequired,
|
view: PropTypes.string.isRequired,
|
||||||
onUpdateSelectedPress: PropTypes.func.isRequired,
|
onUpdateSelectedPress: PropTypes.func.isRequired,
|
||||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
dispatchFetchMovieCollections: PropTypes.func.isRequired,
|
||||||
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
dispatchFetchQueueDetails: PropTypes.func.isRequired,
|
||||||
dispatchClearQueueDetails: PropTypes.func.isRequired
|
dispatchClearQueueDetails: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import FilterModal from 'Components/Filter/FilterModal';
|
|
||||||
import { setMovieCollectionsFilter } from 'Store/Actions/movieCollectionActions';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.movieCollections.items,
|
|
||||||
(state) => state.movieCollections.filterBuilderProps,
|
|
||||||
(sectionItems, filterBuilderProps) => {
|
|
||||||
return {
|
|
||||||
sectionItems,
|
|
||||||
filterBuilderProps,
|
|
||||||
customFilterType: 'movieCollections'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchSetFilter: setMovieCollectionsFilter
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
|
||||||
|
|
@ -1,296 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import AvailabilitySelectInput from 'Components/Form/AvailabilitySelectInput';
|
|
||||||
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
|
|
||||||
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
|
|
||||||
import SelectInput from 'Components/Form/SelectInput';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import CollectionFooterLabel from './CollectionFooterLabel';
|
|
||||||
import styles from './CollectionFooter.css';
|
|
||||||
|
|
||||||
const NO_CHANGE = 'noChange';
|
|
||||||
|
|
||||||
const monitoredOptions = [
|
|
||||||
{
|
|
||||||
key: NO_CHANGE,
|
|
||||||
get value() {
|
|
||||||
return translate('NoChange');
|
|
||||||
},
|
|
||||||
isDisabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'monitored',
|
|
||||||
get value() {
|
|
||||||
return translate('Monitored');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'unmonitored',
|
|
||||||
get value() {
|
|
||||||
return translate('Unmonitored');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const searchOnAddOptions = [
|
|
||||||
{
|
|
||||||
key: NO_CHANGE,
|
|
||||||
get value() {
|
|
||||||
return translate('NoChange');
|
|
||||||
},
|
|
||||||
isDisabled: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'yes',
|
|
||||||
get value() {
|
|
||||||
return translate('Yes');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'no',
|
|
||||||
get value() {
|
|
||||||
return translate('No');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
class CollectionFooter extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
monitored: NO_CHANGE,
|
|
||||||
monitor: NO_CHANGE,
|
|
||||||
qualityProfileId: NO_CHANGE,
|
|
||||||
minimumAvailability: NO_CHANGE,
|
|
||||||
rootFolderPath: NO_CHANGE,
|
|
||||||
searchOnAdd: NO_CHANGE
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
isSaving,
|
|
||||||
saveError
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const newState = {};
|
|
||||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
|
||||||
this.setState({
|
|
||||||
monitored: NO_CHANGE,
|
|
||||||
monitor: NO_CHANGE,
|
|
||||||
qualityProfileId: NO_CHANGE,
|
|
||||||
minimumAvailability: NO_CHANGE,
|
|
||||||
rootFolderPath: NO_CHANGE,
|
|
||||||
searchOnAdd: NO_CHANGE
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.isEmpty(newState)) {
|
|
||||||
this.setState(newState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.setState({ [name]: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onUpdateSelectedPress = () => {
|
|
||||||
const {
|
|
||||||
monitored,
|
|
||||||
monitor,
|
|
||||||
qualityProfileId,
|
|
||||||
minimumAvailability,
|
|
||||||
rootFolderPath,
|
|
||||||
searchOnAdd
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const changes = {};
|
|
||||||
|
|
||||||
if (monitored !== NO_CHANGE) {
|
|
||||||
changes.monitored = monitored === 'monitored';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (monitor !== NO_CHANGE) {
|
|
||||||
changes.monitor = monitor;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (qualityProfileId !== NO_CHANGE) {
|
|
||||||
changes.qualityProfileId = qualityProfileId;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (minimumAvailability !== NO_CHANGE) {
|
|
||||||
changes.minimumAvailability = minimumAvailability;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rootFolderPath !== NO_CHANGE) {
|
|
||||||
changes.rootFolderPath = rootFolderPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (searchOnAdd !== NO_CHANGE) {
|
|
||||||
changes.searchOnAdd = searchOnAdd === 'yes';
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onUpdateSelectedPress(changes);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
selectedIds,
|
|
||||||
isSaving
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
monitored,
|
|
||||||
monitor,
|
|
||||||
qualityProfileId,
|
|
||||||
minimumAvailability,
|
|
||||||
rootFolderPath,
|
|
||||||
searchOnAdd
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const selectedCount = selectedIds.length;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContentFooter>
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('MonitorCollection')}
|
|
||||||
isSaving={isSaving && monitored !== NO_CHANGE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
name="monitored"
|
|
||||||
value={monitored}
|
|
||||||
values={monitoredOptions}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('MonitorMovies')}
|
|
||||||
isSaving={isSaving && monitor !== NO_CHANGE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
name="monitor"
|
|
||||||
value={monitor}
|
|
||||||
values={monitoredOptions}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('QualityProfile')}
|
|
||||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<QualityProfileSelectInputConnector
|
|
||||||
name="qualityProfileId"
|
|
||||||
value={qualityProfileId}
|
|
||||||
includeNoChange={true}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('MinimumAvailability')}
|
|
||||||
isSaving={isSaving && minimumAvailability !== NO_CHANGE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<AvailabilitySelectInput
|
|
||||||
name="minimumAvailability"
|
|
||||||
value={minimumAvailability}
|
|
||||||
includeNoChange={true}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('RootFolder')}
|
|
||||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RootFolderSelectInputConnector
|
|
||||||
name="rootFolderPath"
|
|
||||||
value={rootFolderPath}
|
|
||||||
includeNoChange={true}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
selectedValueOptions={{ includeFreeSpace: false }}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('SearchMoviesOnAdd')}
|
|
||||||
isSaving={isSaving && searchOnAdd !== NO_CHANGE}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SelectInput
|
|
||||||
name="searchOnAdd"
|
|
||||||
value={searchOnAdd}
|
|
||||||
values={searchOnAddOptions}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.buttonContainer}>
|
|
||||||
<div className={styles.buttonContainerContent}>
|
|
||||||
<CollectionFooterLabel
|
|
||||||
label={translate('CountCollectionsSelected', { count: selectedCount })}
|
|
||||||
isSaving={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.buttons}>
|
|
||||||
<div>
|
|
||||||
<SpinnerButton
|
|
||||||
className={styles.addSelectedButton}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
isSpinning={isSaving}
|
|
||||||
isDisabled={!selectedCount || isSaving}
|
|
||||||
onPress={this.onUpdateSelectedPress}
|
|
||||||
>
|
|
||||||
{translate('UpdateSelected')}
|
|
||||||
</SpinnerButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</PageContentFooter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CollectionFooter.propTypes = {
|
|
||||||
selectedIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
||||||
isAdding: PropTypes.bool.isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
onUpdateSelectedPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CollectionFooter;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue