1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-05 13:20:20 -05:00

Compare commits

..

1 Commits

Author SHA1 Message Date
Taloth Saldono
8502f523e6 Fixed: Regression causing updater to fail and added measure to restore bad update 2020-10-11 19:49:21 +02:00
1063 changed files with 30525 additions and 29939 deletions

41
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,41 @@
<!--
Before opening a new issue, please ensure:
- You use the forums for support/questions
- You search for existing bugs/feature requests
- Remove extraneous template details
- Do not prefix title with type of issue (Feature Request, Bug, etc.) The appropriate labels will be added during triage.
-->
## Support / Questions
Please use https://forums.sonarr.tv/ for support. Support requests or questions will be redirected to the forums and the issue will be closed.
<!--
Remove if not opening a bug report
-->
## Bug Report
### System Information/Logs
**Sonarr Version:**
**Operating System:**
**.net Framework (Windows) or mono (macOS/Linux) Version:**
**Link to Log Files (debug or trace):**
**Browser (for UI bugs):**
### Additional Information
<!--
Remove if not opening a feature request
-->
## Feature Request
### What problem are you looking to solve?
### Other Information

28
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,28 @@
---
name: Bug report
about: Create a report to help us improve Sonarr
---
**Describe the bug**
A clear and concise description of what the bug is.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Link to debug or trace log files.
**System Information**
- Sonarr Version: [e.g. 2.0.0.1]
- Operating System: [e.g. Windows 10]
- .net Framework (Windows) or mono (macOS/Linux) Version: [e.g. 4.5 or 5.12]
**UI Bugs:**
- OS: [e.g. Windows]
- Browser: [e.g. chrome, firefox]
- Version: [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -1,82 +0,0 @@
name: Bug Report
description: 'Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Exceptions do not mean you found a bug!'
labels: ['needs-triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: false
- type: textarea
attributes:
label: Environment
description: |
examples:
- **OS**: Ubuntu 20.04
- **Sonarr**: Sonarr 3.0.6.1265
- **Docker Install**: Yes
- **Using Reverse Proxy**: No
- **Browser**: Firefox 90 (If UI related)
value: |
- OS:
- Sonarr:
- Docker Install:
- Using Reverse Proxy:
- Browser:
render: markdown
validations:
required: true
- type: dropdown
attributes:
label: What branch are you running?
options:
- Main
- Develop
- Other (This issue will be closed)
validations:
required: true
- type: textarea
attributes:
label: Trace Logs?
description: |
Trace Logs (https://wiki.servarr.com/sonarr/troubleshooting#logging-and-log-files)
***Generally speaking, all bug reports must have trace logs provided.***
*** Info Logs are not trace logs. If the logs do not say trace and are not from a file like `*.trace.*.txt` they are not trace logs.***
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Links? Screenshots? References? Anything that will give us more context about the issue you are encountering!
***Generally speaking, all bug reports must have trace logs provided.***
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: true
required: true

View File

@@ -1,14 +0,0 @@
blank_issues_enabled: false
contact_links:
- name: Support via Discord
url: https://discord.gg/M6BvZn5
about: Chat with users and devs on support and setup related topics.
- name: Support via Reddit
url: https://reddit.com/r/Sonarr
about: Discuss and search through support topics.
- name: Support via Forums
url: https://forums.sonarr.tv/
about: Discuss and search through support topics.
- name: Support via IRC
url: https://web.libera.chat/?channels=#sonarr
about: Chat with users and devs on support and setup related topics.

View File

@@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest an idea for Sonarr
---
**Describe the problem**
A clear and concise description of the problem you're looking to solve.
**Describe any solutions you think might work**
A clear and concise description of any solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,38 +0,0 @@
name: Feature Request
description: 'Suggest an idea for Sonarr'
labels: ['needs-triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the feature you are requesting.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe
description: A clear and concise description of what the problem is.
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Mockups? Anything that will give us more context about the feature you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: true

View File

@@ -0,0 +1,7 @@
---
name: Other issues
about: How to get support or ask questions
---
Please use https://forums.sonarr.tv/ for support. Support requests or questions will be redirected to the forums and the issue will be closed.

View File

@@ -6,7 +6,7 @@ A few sentences describing the overall goals of the pull request's commits.
#### Todos
- [ ] Tests
- [ ] Wiki Updates
- [ ] Documentation
#### Issues Fixed or Closed by this PR

6
.github/SUPPORT.md vendored
View File

@@ -1,7 +1,7 @@
## Support
There are a number of frequently asked questions that have been answered in our [FAQ](https://wiki.servarr.com/sonarr/faq)
There are a number of frequently asked questions that have been answered in our [FAQ](https://github.com/Sonarr/Sonarr/wiki/FAQ)
The [wiki](https://wiki.servarr.com/sonarr) contains other information and guides
The [wiki](https://github.com/Sonarr/Sonarr/wiki) contains other information and guides
Please use one of the support channels: [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord ](https://discord.gg/M6BvZn5), or [IRC](https://web.libera.chat/?channels=#sonarr)for support/questions.
If you have a support question, please use the [support forums](https://forums.sonarr.tv/).

View File

@@ -1,21 +0,0 @@
name: 'Lock threads'
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '90'
issue-exclude-created-before: ''
issue-exclude-labels: 'one-day-maybe'
issue-lock-labels: ''
issue-lock-comment: ''
issue-lock-reason: 'resolved'
process-only: ''

1
.gitignore vendored
View File

@@ -141,4 +141,3 @@ output/*
_start
src/.idea/
/distribution/windows/setup/output/*

View File

@@ -3,40 +3,25 @@
We're always looking for people to help make Sonarr even better, there are a number of ways to contribute.
## Documentation ##
Setup guides, [FAQ](https://wiki.servarr.com/sonarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/sonarr) the better.
Setup guides, FAQ, the more information we have on the wiki the better.
## Development ##
### Tools required ###
- Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works (https://www.visualstudio.com/downloads/).
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
- [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download/) (Node 10.X.X or higher)
- [Yarn](https://yarnpkg.com/)
### Getting started ###
1. Fork Sonarr
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/get-started/quickstart/fork-a-repo)
3. Install the required Node Packages `yarn install`
4. Start webpack to monitor your dev environment for any frontend changes that need post processing using `yarn start` command.
5. Build the project in Visual Studio, Setting startup project to `Sonarr.Console` and framework to `x86`
6. Debug the project in Visual Studio
7. Open http://localhost:8989
See the readme for information on setting up your development environment.
### Contributing Code ###
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Sonarr/Sonarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
- Rebase from Sonarr's `develop` branch, don't merge
- Rebase from Sonarr's develop branch, don't 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 our [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord](https://discord.gg/Ex7FmFK), or [IRC](https://web.libera.chat/?channels=#sonarr) if you have any questions
- Reach out to us on the forums or on IRC 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 should be the default for VS 2019 and WebStorm
- Use 4 spaces instead of tabs, this is the default for VS 2012 and WebStorm (to my knowledge)
### Pull Requesting ###
- Only make pull requests to develop (currently `develop`), never `main`, if you make a PR to master we'll comment on it and close it
- Only make pull requests to develop, never master, if you make a PR to master we'll 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)

View File

@@ -4,23 +4,17 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
## Getting Started
- [Download/Installation](https://sonarr.tv/#downloads-v3)
- [FAQ](https://wiki.servarr.com/sonarr/faq)
- [Wiki](https://wiki.servarr.com/Sonarr)
- [(WIP) API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
- [Donate](https://sonarr.tv/donate)
- [Download](https://sonarr.tv/#download) (Linux, MacOS, Windows, Docker, etc.)
- [Installation](https://github.com/Sonarr/Sonarr/wiki/Installation)
- [FAQ](https://github.com/Sonarr/Sonarr/wiki/FAQ)
- [Wiki](https://github.com/Sonarr/Sonarr/wiki)
- [API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
## Support
Note: GitHub Issues are for Bugs and Feature Requests Only
- [Forums](https://forums.sonarr.tv/)
- [Donate](https://sonarr.tv/donate)
- [Discord](https://discord.gg/M6BvZn5)
- [GitHub - Bugs and Feature Requests Only](https://github.com/Sonarr/Sonarr/issues)
- [IRC](https://web.libera.chat/?channels=#sonarr)
- [Reddit](https://www.reddit.com/r/sonarr)
- [Wiki](https://wiki.servarr.com/sonarr)
## Features
@@ -38,29 +32,55 @@ Note: GitHub Issues are for Bugs and Feature Requests Only
- Full support for specials and multi-episode releases
- And a beautiful UI
## Contributing
## Configuring Development Environment:
### Development
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
### Requirements
<a href="https://github.com/Sonarr/Sonarr/graphs/contributors"><img src="https://opencollective.com/Sonarr/contributors.svg?width=890&button=false" /></a>
- [Visual Studio 2017](https://www.visualstudio.com/vs)
- [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download)
- [Yarn](https://yarnpkg.com)
### Setup
- Make sure all the required software mentioned above are installed
- Clone the repository recursively to get Sonarr and it's submodules
- You can do this by running `git clone --recursive https://github.com/Sonarr/Sonarr.git`
- Install the required Node Packages using `yarn`
### Backend Development
- Run `yarn build` to build the UI
- Open `Sonarr.sln` in Visual Studio
- Make sure `Sonarr.Console` is set as the startup project
- Build `Sonarr.Windows` and `Sonarr.Mono` projects
- Build Solution
### UI Development
- Run `yarn watch` to build UI and rebuild automatically when changes are detected
- Run Sonarr.Console.exe (or debug in Visual Studio)
### Licenses
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
- Copyright 2010-2020
### Supporters
This project would not be possible without the support of our users and software providers.
[**Become a sponsor or backer**](https://opencollective.com/sonarr) to help us out!
#### Mega Sponsors
[![Sponsors](https://opencollective.com/sonarr/tiers/mega-sponsor.svg?width=890)](https://opencollective.com/sonarr/contribute/mega-sponsor-21443/checkout)
This project would not be possible without the support of our users and software providers. [**Become a sponsor or backer**](https://opencollective.com/sonarr) to help us out!
#### Sponsors
[![Flexible Sponsors](https://opencollective.com/sonarr/sponsors.svg?width=890)](https://opencollective.com/sonarr/contribute/sponsor-21457/checkout)
[![Sponsors](https://opencollective.com/sonarr/tiers/sponsor.svg)](https://opencollective.com/sonarr/contribute/sponsor-21443/checkout)
#### Flexible Sponsors
[![Flexible Sponsors](https://opencollective.com/sonarr/tiers/flexible-sponsor.svg?avatarHeight=54)](https://opencollective.com/sonarr/contribute/flexible-sponsor-21457/checkout)
#### Backers
[![Backers](https://opencollective.com/sonarr/backers.svg?width=890)](https://opencollective.com/sonarr/contribute/backer-21442/checkout)
[![Backers](https://opencollective.com/sonarr/tiers/backer.svg?avatarHeight=48)](https://opencollective.com/sonarr/contribute/backer-21442/checkout)
#### JetBrains
@@ -70,7 +90,3 @@ Thank you to [<img src="/Logo/Jetbrains/jetbrains.svg" alt="JetBrains" width="32
* [<img src="/Logo/Jetbrains/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
* [<img src="/Logo/Jetbrains/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
### Licenses
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
- Copyright 2010-2021

View File

@@ -12,11 +12,8 @@ sourceFolder='./src'
slnFile=$sourceFolder/Sonarr.sln
updateSubFolder=Sonarr.Update
sqlitePackageDir="$HOME/.nuget/packages/system.data.sqlite.core.servarr/1.0.115.5-18"
nuget='tools/nuget/nuget.exe';
vswhere='tools/vswhere/vswhere.exe';
macho='tools/macho/MachOConverter.exe';
. ./version.sh
@@ -138,16 +135,13 @@ Build()
CleanFolder $outputFolder false
echo "Removing Sonarr.Update/sqlite3.dll"
rm $outputFolder/Sonarr.Update/sqlite3.dll
echo "Removing Mono.Posix.dll"
rm $outputFolder/Mono.Posix.dll
ProgressEnd 'Build'
}
RunWebpack()
RunGulp()
{
ProgressStart 'yarn install'
yarn install
@@ -155,9 +149,9 @@ RunWebpack()
LintUI
ProgressStart 'Running webpack'
CheckExitCode yarn run build --env production
ProgressEnd 'Running webpack'
ProgressStart 'Running gulp'
CheckExitCode yarn run build --production
ProgressEnd 'Running gulp'
}
CreateMdbs()
@@ -240,7 +234,6 @@ PackageMono()
echo "Removing native windows binaries Sqlite, MediaInfo"
rm -f $outputFolderLinux/sqlite3.*
rm -f $outputFolderLinux/Sonarr.Update/sqlite3.*
rm -f $outputFolderLinux/MediaInfo.*
PatchMono $outputFolderLinux
@@ -285,22 +278,17 @@ PackageMacOS()
chmod +x $outputFolderMacOS/Sonarr
echo "Adding Sonarr.Update Launcher"
CheckExitCode cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOS/Sonarr.Update/
CheckExitCode mv $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe.bak
CheckExitCode mv $outputFolderMacOS/Sonarr.Update/Launcher $outputFolderMacOS/Sonarr.Update/Sonarr.Update
CheckExitCode mv $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe.bak $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe
cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOS/Sonarr.Update/
mv $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe.bak
mv $outputFolderMacOS/Sonarr.Update/Launcher $outputFolderMacOS/Sonarr.Update/Sonarr.Update
mv $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe.bak $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe
chmod +x $outputFolderMacOS/Sonarr.Update/Sonarr.Update
echo "Adding sqlite dylib"
CheckExitCode cp "$sqlitePackageDir/runtimes/osx-x64/native/net46"/*.config $outputFolderMacOS/
if [ $runtime = "dotnet" ] ; then
CheckExitCode $macho merge $outputFolderMacOS "$sqlitePackageDir/runtimes/osx-x64/native/net46"/*.dylib "$sqlitePackageDir/runtimes/osx-arm64/native/net46"/*.dylib
else
CheckExitCode mono $macho merge $outputFolderMacOS "$sqlitePackageDir/runtimes/osx-x64/native/net46"/*.dylib "$sqlitePackageDir/runtimes/osx-arm64/native/net46"/*.dylib
fi
echo "Adding sqlite dylibs"
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOS
echo "Adding MediaInfo dylib"
CheckExitCode cp $sourceFolder/Libraries/MediaInfo/x64/*.dylib $outputFolderMacOS/
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOS
ProgressEnd 'Creating MacOS Package'
}
@@ -309,37 +297,29 @@ PackageMacOSApp()
{
ProgressStart 'Creating macOS App Package'
outputFolderMacOSAppBase=$outputFolderMacOSApp/Sonarr.app/Contents/MacOS
outputFolderMacOSAppBin=$outputFolderMacOSAppBase/bin
rm -rf $outputFolderMacOSApp
mkdir $outputFolderMacOSApp
cp -r ./distribution/osx/Sonarr.app $outputFolderMacOSApp
mkdir -p $outputFolderMacOSAppBase
mkdir -p $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
echo "Adding Sonarr Launcher"
CheckExitCode cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOSAppBase/
CheckExitCode mv $outputFolderMacOSAppBase/Launcher $outputFolderMacOSAppBase/Sonarr
chmod +x $outputFolderMacOSAppBase/Sonarr
cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/
mv $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Launcher $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Sonarr
chmod +x $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Sonarr
echo "Copying Binaries"
mkdir -p $outputFolderMacOSAppBin
CheckExitCode cp -r $outputFolderLinux/* $outputFolderMacOSAppBin
echo "Adding sqlite dylib"
if [ $runtime = "dotnet" ] ; then
CheckExitCode $macho merge $outputFolderMacOSAppBin "$sqlitePackageDir/runtimes/osx-x64/native/net46" "$sqlitePackageDir/runtimes/osx-arm64/native/net46"
else
CheckExitCode mono $macho merge $outputFolderMacOSAppBin "$sqlitePackageDir/runtimes/osx-x64/native/net46" "$sqlitePackageDir/runtimes/osx-arm64/native/net46"
fi
mkdir -p $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin
cp -r $outputFolderLinux/* $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/
echo "Adding sqlite dylibs"
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/
echo "Adding MediaInfo dylib"
CheckExitCode cp $sourceFolder/Libraries/MediaInfo/x64/*.dylib $outputFolderMacOSAppBin/
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/
echo "Removing Update Folder"
rm -r $outputFolderMacOSAppBin/Sonarr.Update
echo "# Do Not Edit\nPackageVersion=${BUILD_NUMBER}\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=${BUILD_NUMBER}\nUpdateMethod=$PackageUpdater\nBranch=${Branch:-master}" > $outputFolderMacOSAppBase/package_info
rm -r $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/Sonarr.Update
echo "# Do Not Edit\nPackageVersion=${BUILD_NUMBER}\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=${BUILD_NUMBER}\nUpdateMethod=$PackageUpdater\nBranch=${Branch:-master}" > $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/package_info
ProgressEnd 'Creating macOS App Package'
}
@@ -467,7 +447,7 @@ esac
UpdateVersionNumber
Build
CreateReleaseInfo
RunWebpack
RunGulp
PackageMono
PackageMacOS
PackageMacOSApp

View File

@@ -1,5 +1,4 @@
fromdos ./debian/*
chmod ugo-x ./debian/*
cp -r ./debian ./debian_backup
BuildVersion=${dependent_build_number:-3.10.0.999}

View File

@@ -1 +1 @@
10
8

View File

@@ -8,10 +8,7 @@ db_input high sonarr/owning_group || true
db_endblock
db_go
db_beginblock
db_input low sonarr/owning_umask || true
db_input low sonarr/config_directory || true
db_endblock
db_go
exit 0

View File

@@ -9,8 +9,6 @@ db_get sonarr/owning_user
USER="$RET"
db_get sonarr/owning_group
GROUP="$RET"
db_get sonarr/owning_umask
UMASK="$RET"
db_get sonarr/config_directory
CONFDIR="$RET"
@@ -66,11 +64,9 @@ fi
# Create data directory
if [ ! -d "$CONFDIR" ]; then
mkdir -p "$CONFDIR"
chown -R $USER:$GROUP "$CONFDIR"
fi
# Set permissions on data directory (always do this instead only on creation in case user was changed via dpkg-reconfigure)
chown -R $USER:$GROUP "$CONFDIR"
#BEGIN BUILTIN UPDATER
# Apply patch if present
if [ "$UPDATER" = "BuiltIn" ] && [ -f /usr/lib/sonarr/bin_patch/release_info ]; then
@@ -96,7 +92,7 @@ fi
chown -R $USER:$GROUP /usr/lib/sonarr
# Update sonarr.service file
sed -i "s:User=\w*:User=$USER:g; s:Group=\w*:Group=$GROUP:g; s:UMask=[0-9]*:UMask=$UMASK:g; s:-data=.*$:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
sed -i "s:User=sonarr:User=$USER:g; s:Group=sonarr:Group=$GROUP:g; s:-data=/var/lib/sonarr:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
#BEGIN BUILTIN UPDATER
if [ "$UPDATER" = "BuiltIn" ]; then

View File

@@ -9,7 +9,7 @@ UPDATER={updater}
# Existing nzbdrone packages do not have startup scripts and the process might still be running.
# If the user manually installed nzbdrone then the process might still be running too.
if [ $1 = "install" ]; then
psNzbDrone=`ps ax -o'user:20,pid,ppid,unit,args' | grep mono.*NzbDrone\\\\.exe || true`
psNzbDrone=`ps ax -o'user,pid,ppid,unit,args' | grep mono.*NzbDrone\\\\.exe || true`
if [ ! -z "$psNzbDrone" ]; then
# Get the user and optional systemd unit
psNzbDroneUser=`echo "$psNzbDrone" | tr -s ' ' | cut -d ' ' -f 1`

View File

@@ -1,3 +1,2 @@
ignores msbuild
ignores libmediainfo0v5
ignores libc6

View File

@@ -1,6 +1,3 @@
# This file is owned by the sonarr package, DO NOT MODIFY MANUALLY
# Instead use 'dpkg-reconfigure -plow sonarr' to modify User/Group/UMask/-data
# Or use systemd built-in override functionality using 'systemctl edit sonarr'
[Unit]
Description=Sonarr Daemon
After=network.target

View File

@@ -14,12 +14,6 @@ Description: Sonarr group:
Any media files created by Sonarr will be writeable by this group.
It's advisable to keep the group the same between download client, Sonarr and media centers.
Template: sonarr/owning_umask
Type: string
Default: 0002
Description: Sonarr umask:
Specifies the umask of the files created by Sonarr. 0002 means the files will be created with 664 as permissions.
Template: sonarr/config_directory
Type: string
Default: /var/lib/sonarr

View File

@@ -1,25 +1,16 @@
FROM ubuntu:focal AS builder
FROM ubuntu:xenial AS builder
ENV DEBIAN_FRONTEND noninteractive
ENV MONO_VERSION 5.18
RUN apt-get update && \
apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends \
apt-transport-https \
wget dirmngr gpg gpg-agent \
# add-apt-repository for PPAs
software-properties-common && \
rm -rf /var/lib/apt/lists/*
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
echo "deb http://download.mono-project.com/repo/debian stable-focal main" > /etc/apt/sources.list.d/mono-official-stable.list && \
echo "deb http://download.mono-project.com/repo/debian stable-xenial/snapshots/$MONO_VERSION main" > /etc/apt/sources.list.d/mono-official-stable.list && \
apt-get update && apt-get install -y \
devscripts build-essential tofrodos \
dh-make dh-systemd \
cli-common-dev \
mono-complete \
sqlite3 libcurl4 mediainfo
RUN apt-get upgrade -y
sqlite3 libcurl3 mediainfo
RUN apt-cache policy mono-complete
RUN apt-cache policy cli-common-dev

View File

@@ -37,7 +37,6 @@ Compression=lzma2/normal
AppContact={#ForumsURL}
VersionInfoVersion={#BuildNumber}
SetupLogging=yes
OutputDir=output
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
@@ -60,12 +59,11 @@ Name: "{userstartup}\{#AppName}"; Filename: "{app}\Sonarr.exe"; WorkingDir: "{ap
[InstallDelete]
Name: "{commonappdata}\NzbDrone\bin"; Type: filesandordirs
Name: "{app}"; Type: filesandordirs
[Run]
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u /exitimmediately"; Flags: runhidden waituntilterminated;
Filename: "{app}\Sonarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl /exitimmediately"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Installing Windows Service"; Parameters: "/i /exitimmediately"; Flags: runhidden waituntilterminated; Tasks: windowsService
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u"; Flags: runhidden waituntilterminated;
Filename: "{app}\Sonarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Installing Windows Service"; Parameters: "/i"; Flags: runhidden waituntilterminated; Tasks: windowsService
Filename: "{app}\Sonarr.exe"; Description: "Open Sonarr Web UI"; Flags: postinstall skipifsilent nowait; Tasks: windowsService;
Filename: "{app}\Sonarr.exe"; Description: "Start Sonarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;

View File

@@ -17,9 +17,6 @@ RUN fromdos /startup.sh
WORKDIR /data/
VOLUME ["/data/_tests_linux", "/data/_output_linux", "/data/_tests_results"]
RUN groupadd sonarrtst -g 4020 && useradd sonarrtst -u 4021 -g 4020 -m -s /bin/bash
USER sonarrtst
CMD bash /startup.sh

View File

@@ -25,8 +25,5 @@ RUN fromdos /startup.sh
WORKDIR /data/
VOLUME ["/data/_tests_linux", "/data/_output_linux", "/data/_tests_results"]
RUN groupadd sonarrtst -g 4020 && useradd sonarrtst -u 4021 -g 4020 -m -s /bin/bash
USER sonarrtst
CMD bash /startup.sh

View File

@@ -75,7 +75,7 @@
"function-parentheses-newline-inside": "never-multi-line",
"function-parentheses-space-inside": "never",
"function-url-quotes": "always",
"function-url-scheme-disallowed-list": [
"function-url-scheme-blacklist": [
"data"
],
"function-whitespace-after": "always",

View File

@@ -1,263 +0,0 @@
const path = require('path');
const webpack = require('webpack');
const FileManagerPlugin = require('filemanager-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const LiveReloadPlugin = require('webpack-livereload-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = (env) => {
const uiFolder = 'UI';
const frontendFolder = path.join(__dirname, '..');
const srcFolder = path.join(frontendFolder, 'src');
const isProduction = !!env.production;
const isProfiling = isProduction && !!env.profile;
const inlineWebWorkers = 'no-fallback';
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
console.log('Source Folder:', srcFolder);
console.log('Output Folder:', distFolder);
console.log('isProduction:', isProduction);
console.log('isProfiling:', isProfiling);
const config = {
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map',
stats: {
children: false
},
watchOptions: {
ignored: /node_modules/
},
entry: {
index: 'index.js'
},
resolve: {
modules: [
srcFolder,
path.join(srcFolder, 'Shims'),
'node_modules'
],
alias: {
jquery: 'jquery/src/jquery',
'react-middle-truncate': 'react-middle-truncate/lib/react-middle-truncate'
}
},
output: {
path: distFolder,
publicPath: '/',
filename: '[name].js',
sourceMapFilename: '[file].map'
},
optimization: {
moduleIds: 'deterministic',
chunkIds: 'named',
splitChunks: {
chunks: 'initial',
name: 'vendors'
}
},
performance: {
hints: false
},
plugins: [
new webpack.DefinePlugin({
__DEV__: !isProduction,
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
}),
new MiniCssExtractPlugin({
filename: 'Content/styles.css'
}),
new HtmlWebpackPlugin({
template: 'frontend/src/index.ejs',
filename: 'index.html',
publicPath: '/'
}),
new FileManagerPlugin({
events: {
onEnd: {
copy: [
// HTML
{
source: 'frontend/src/*.html',
destination: distFolder
},
// Fonts
{
source: 'frontend/src/Content/Fonts/*.*',
destination: path.join(distFolder, 'Content/Fonts')
},
// Icon Images
{
source: 'frontend/src/Content/Images/Icons/*.*',
destination: path.join(distFolder, 'Content/Images/Icons')
},
// Images
{
source: 'frontend/src/Content/Images/*.*',
destination: path.join(distFolder, 'Content/Images')
},
// Robots
{
source: 'frontend/src/Content/robots.txt',
destination: path.join(distFolder, 'Content/robots.txt')
}
]
}
}
}),
new LiveReloadPlugin()
],
resolveLoader: {
modules: [
'node_modules',
'frontend/build/webpack/'
]
},
module: {
rules: [
{
test: /\.worker\.js$/,
use: {
loader: 'worker-loader',
options: {
filename: '[name].js',
inline: inlineWebWorkers
}
}
},
{
test: /\.js?$/,
exclude: /(node_modules|JsLibraries)/,
use: [
{
loader: 'babel-loader',
options: {
configFile: `${frontendFolder}/babel.config.js`,
envName: isProduction ? 'production' : 'development',
presets: [
[
'@babel/preset-env',
{
modules: false,
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: 3
}
]
]
}
}
]
},
// CSS Modules
{
test: /\.css$/,
exclude: /(node_modules|globals.css)/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[name]/[local]/[hash:base64:5]'
}
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
config: 'frontend/postcss.config.js'
}
}
}
]
},
// Global styles
{
test: /\.css$/,
include: /(node_modules|globals.css)/,
use: [
'style-loader',
{
loader: 'css-loader'
}
]
},
// Fonts
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
mimetype: 'application/font-woff',
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
},
{
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'file-loader',
options: {
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
}
]
}
};
if (isProfiling) {
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
config.optimization.minimizer = [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
mangle: false,
keep_classnames: true,
keep_fnames: true
}
})
];
}
return config;
};

17
frontend/gulp/build.js Normal file
View File

@@ -0,0 +1,17 @@
const gulp = require('gulp');
require('./clean');
require('./copy');
require('./webpack');
gulp.task('build',
gulp.series('clean',
gulp.parallel(
'webpack',
'copyHtml',
'copyFonts',
'copyImages'
)
)
);

8
frontend/gulp/clean.js Normal file
View File

@@ -0,0 +1,8 @@
const gulp = require('gulp');
const del = require('del');
const paths = require('./helpers/paths');
gulp.task('clean', () => {
return del([paths.dest.root]);
});

34
frontend/gulp/copy.js Normal file
View File

@@ -0,0 +1,34 @@
const path = require('path');
const gulp = require('gulp');
const print = require('gulp-print').default;
const cache = require('gulp-cached');
const livereload = require('gulp-livereload');
const paths = require('./helpers/paths.js');
gulp.task('copyHtml', () => {
return gulp.src(paths.src.html, { base: paths.src.root })
.pipe(cache('copyHtml'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});
gulp.task('copyFonts', () => {
return gulp.src(
path.join(paths.src.fonts, '**', '*.*'), { base: paths.src.root }
)
.pipe(cache('copyFonts'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});
gulp.task('copyImages', () => {
return gulp.src(
path.join(paths.src.images, '**', '*.*'), { base: paths.src.root }
)
.pipe(cache('copyImages'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});

View File

@@ -0,0 +1,5 @@
require('./build.js');
require('./clean.js');
require('./copy.js');
require('./watch.js');
require('./webpack.js');

View File

@@ -0,0 +1,6 @@
const colors = require('ansi-colors');
module.exports = function errorHandler(error) {
console.log(colors.red(`Error (${error.plugin}): ${error.message}`));
this.emit('end');
};

View File

@@ -0,0 +1,23 @@
const root = './frontend/src';
const paths = {
src: {
root,
html: `${root}/*.html`,
scripts: `${root}/**/*.js`,
content: `${root}/Content/`,
fonts: `${root}/Content/Fonts/`,
images: `${root}/Content/Images/`,
exclude: {
libs: `!${root}/JsLibraries/**`
}
},
dest: {
root: './_output/UI/',
content: './_output/UI/Content/',
fonts: './_output/UI/Content/Fonts/',
images: './_output/UI/Content/Images/'
}
};
module.exports = paths;

18
frontend/gulp/watch.js Normal file
View File

@@ -0,0 +1,18 @@
const gulp = require('gulp');
const livereload = require('gulp-livereload');
const gulpWatch = require('gulp-watch');
const paths = require('./helpers/paths.js');
require('./copy.js');
require('./webpack.js');
function watch() {
livereload.listen({ start: true });
gulp.task('webpackWatch')();
gulpWatch(paths.src.html, gulp.series('copyHtml'));
gulpWatch(`${paths.src.fonts}**/*.*`, gulp.series('copyFonts'));
gulpWatch(`${paths.src.images}**/*.*`, gulp.series('copyImages'));
}
gulp.task('watch', gulp.series('build', watch));

268
frontend/gulp/webpack.js Normal file
View File

@@ -0,0 +1,268 @@
const gulp = require('gulp');
const webpackStream = require('webpack-stream');
const livereload = require('gulp-livereload');
const path = require('path');
const webpack = require('webpack');
const errorHandler = require('./helpers/errorHandler');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackPluginHtmlTags = require('html-webpack-plugin/lib/html-tags');
const TerserPlugin = require('terser-webpack-plugin');
const uiFolder = 'UI';
const frontendFolder = path.join(__dirname, '..');
const srcFolder = path.join(frontendFolder, 'src');
const isProduction = process.argv.indexOf('--production') > -1;
const isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
const inlineWebWorkers = 'no-fallback';
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
console.log('Source Folder:', srcFolder);
console.log('Output Folder:', distFolder);
console.log('isProduction:', isProduction);
console.log('isProfiling:', isProfiling);
const cssVarsFiles = [
'../src/Styles/Variables/colors',
'../src/Styles/Variables/dimensions',
'../src/Styles/Variables/fonts',
'../src/Styles/Variables/animations',
'../src/Styles/Variables/zIndexes'
].map(require.resolve);
// Override the way HtmlWebpackPlugin injects the scripts
// TODO: Find a better way to get these paths without
HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets, assetTags) {
const head = assetTags.headTags.map((v) => {
const href = v.attributes.href
.replace('\\', '/')
.replace('%5C', '/');
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${href}` };
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
});
const body = assetTags.bodyTags.map((v) => {
v.attributes = { src: `/${v.attributes.src}` };
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
});
return html
.replace('<!-- webpack bundles head -->', head.join('\r\n '))
.replace('<!-- webpack bundles body -->', body.join('\r\n '));
};
const plugins = [
new webpack.DefinePlugin({
__DEV__: !isProduction,
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
}),
new MiniCssExtractPlugin({
filename: path.join('Content', 'styles.css')
}),
new HtmlWebpackPlugin({
template: 'frontend/src/index.html',
filename: 'index.html'
})
];
const config = {
mode: isProduction ? 'production' : 'development',
devtool: '#source-map',
stats: {
children: false
},
watchOptions: {
ignored: /node_modules/
},
entry: {
index: 'index.js'
},
resolve: {
modules: [
srcFolder,
path.join(srcFolder, 'Shims'),
'node_modules'
],
alias: {
jquery: 'jquery/src/jquery'
}
},
output: {
path: distFolder,
filename: '[name].js',
sourceMapFilename: '[file].map'
},
optimization: {
chunkIds: 'named',
splitChunks: {
chunks: 'initial'
}
},
performance: {
hints: false
},
plugins,
resolveLoader: {
modules: [
'node_modules',
'frontend/gulp/webpack/'
]
},
module: {
rules: [
{
test: /\.worker\.js$/,
use: {
loader: 'worker-loader',
options: {
filename: '[name].js',
inline: inlineWebWorkers
}
}
},
{
test: /\.js?$/,
exclude: /(node_modules|JsLibraries)/,
use: [
{
loader: 'babel-loader',
options: {
configFile: `${frontendFolder}/babel.config.js`,
envName: isProduction ? 'production' : 'development',
presets: [
[
'@babel/preset-env',
{
modules: false,
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: 3
}
]
]
}
}
]
},
// CSS Modules
{
test: /\.css$/,
exclude: /(node_modules|globals.css)/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[name]/[local]/[hash:base64:5]'
}
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
config: {
ctx: {
cssVarsFiles
},
path: 'frontend/postcss.config.js'
}
}
}
]
},
// Global styles
{
test: /\.css$/,
include: /(node_modules|globals.css)/,
use: [
'style-loader',
{
loader: 'css-loader'
}
]
},
// Fonts
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
mimetype: 'application/font-woff',
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
},
{
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'file-loader',
options: {
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
}
]
}
};
if (isProfiling) {
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
config.optimization.minimizer = [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
mangle: false,
keep_classnames: true,
keep_fnames: true
}
})
];
}
gulp.task('webpack', () => {
return webpackStream(config)
.pipe(gulp.dest('_output/UI'));
});
gulp.task('webpackWatch', () => {
config.watch = true;
return webpackStream(config)
.on('error', errorHandler)
.pipe(gulp.dest('_output/UI'))
.on('error', errorHandler)
.pipe(livereload())
.on('error', errorHandler);
});

View File

@@ -1,32 +1,23 @@
const reload = require('require-nocache')(module);
const cssVarsFiles = [
'./src/Styles/Variables/colors',
'./src/Styles/Variables/dimensions',
'./src/Styles/Variables/fonts',
'./src/Styles/Variables/animations',
'./src/Styles/Variables/zIndexes'
].map(require.resolve);
module.exports = (ctx, configPath, options) => {
const config = {
plugins: {
'postcss-mixins': {
mixinsDir: [
'frontend/src/Styles/Mixins'
]
},
'postcss-simple-vars': {
variables: () =>
ctx.options.cssVarsFiles.reduce((acc, vars) => {
return Object.assign(acc, reload(vars));
}, {})
},
'postcss-color-function': {},
'postcss-nested': {}
}
};
const mixinsFiles = [
'frontend/src/Styles/Mixins/cover.css',
'frontend/src/Styles/Mixins/linkOverlay.css',
'frontend/src/Styles/Mixins/scroller.css',
'frontend/src/Styles/Mixins/truncate.css'
];
module.exports = {
plugins: [
['postcss-mixins', {
mixinsFiles
}],
['postcss-simple-vars', {
variables: () =>
cssVarsFiles.reduce((acc, vars) => {
return Object.assign(acc, reload(vars));
}, {})
}],
'postcss-color-function',
'postcss-nested'
]
return config;
};

View File

@@ -0,0 +1,123 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { align, icons } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import BlacklistRowConnector from './BlacklistRowConnector';
class Blacklist extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
columns,
totalRecords,
isClearingBlacklistExecuting,
onClearBlacklistPress,
...otherProps
} = this.props;
return (
<PageContent title="Blacklist">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Clear"
iconName={icons.CLEAR}
isSpinning={isClearingBlacklistExecuting}
onPress={onClearBlacklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to load blacklist</div>
}
{
isPopulated && !error && !items.length &&
<div>
No history blacklist
</div>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
columns={columns}
{...otherProps}
>
<TableBody>
{
items.map((item) => {
return (
<BlacklistRowConnector
key={item.id}
columns={columns}
{...item}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
</div>
}
</PageContentBody>
</PageContent>
);
}
}
Blacklist.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
onClearBlacklistPress: PropTypes.func.isRequired
};
export default Blacklist;

View File

@@ -0,0 +1,154 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import withCurrentPage from 'Components/withCurrentPage';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import * as blacklistActions from 'Store/Actions/blacklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import Blacklist from './Blacklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blacklist,
createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
(blacklist, isClearingBlacklistExecuting) => {
return {
isClearingBlacklistExecuting,
...blacklist
};
}
);
}
const mapDispatchToProps = {
...blacklistActions,
executeCommand
};
class BlacklistConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchBlacklist,
gotoBlacklistFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchBlacklist();
} else {
gotoBlacklistFirstPage();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
this.props.gotoBlacklistFirstPage();
}
}
componentWillUnmount() {
this.props.clearBlacklist();
unregisterPagePopulator(this.repopulate);
}
//
// Control
repopulate = () => {
this.props.fetchBlacklist();
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoBlacklistFirstPage();
}
onPreviousPagePress = () => {
this.props.gotoBlacklistPreviousPage();
}
onNextPagePress = () => {
this.props.gotoBlacklistNextPage();
}
onLastPagePress = () => {
this.props.gotoBlacklistLastPage();
}
onPageSelect = (page) => {
this.props.gotoBlacklistPage({ page });
}
onSortPress = (sortKey) => {
this.props.setBlacklistSort({ sortKey });
}
onTableOptionChange = (payload) => {
this.props.setBlacklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlacklistFirstPage();
}
}
onClearBlacklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
}
onTableOptionChange = (payload) => {
this.props.setBlacklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlacklistFirstPage();
}
}
//
// Render
render() {
return (
<Blacklist
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlacklistPress={this.onClearBlacklistPress}
{...this.props}
/>
);
}
}
BlacklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlacklist: PropTypes.func.isRequired,
gotoBlacklistFirstPage: PropTypes.func.isRequired,
gotoBlacklistPreviousPage: PropTypes.func.isRequired,
gotoBlacklistNextPage: PropTypes.func.isRequired,
gotoBlacklistLastPage: PropTypes.func.isRequired,
gotoBlacklistPage: PropTypes.func.isRequired,
setBlacklistSort: PropTypes.func.isRequired,
setBlacklistTableOption: PropTypes.func.isRequired,
clearBlacklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector)
);

View File

@@ -9,7 +9,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
class BlocklistDetailsModal extends Component {
class BlacklistDetailsModal extends Component {
//
// Render
@@ -77,7 +77,7 @@ class BlocklistDetailsModal extends Component {
}
}
BlocklistDetailsModal.propTypes = {
BlacklistDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
@@ -86,4 +86,4 @@ BlocklistDetailsModal.propTypes = {
onModalClose: PropTypes.func.isRequired
};
export default BlocklistDetailsModal;
export default BlacklistDetailsModal;

View File

@@ -2,17 +2,16 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import EpisodeLanguage from 'Episode/EpisodeLanguage';
import EpisodeQuality from 'Episode/EpisodeQuality';
import SeriesTitleLink from 'Series/SeriesTitleLink';
import BlocklistDetailsModal from './BlocklistDetailsModal';
import styles from './BlocklistRow.css';
import BlacklistDetailsModal from './BlacklistDetailsModal';
import styles from './BlacklistRow.css';
class BlocklistRow extends Component {
class BlacklistRow extends Component {
//
// Lifecycle
@@ -41,7 +40,6 @@ class BlocklistRow extends Component {
render() {
const {
id,
series,
sourceTitle,
language,
@@ -50,20 +48,12 @@ class BlocklistRow extends Component {
protocol,
indexer,
message,
isSelected,
columns,
onSelectedChange,
onRemovePress
} = this.props;
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{
columns.map((column) => {
const {
@@ -152,7 +142,7 @@ class BlocklistRow extends Component {
/>
<IconButton
title="Remove from blocklist"
title="Remove from blacklist"
name={icons.REMOVE}
kind={kinds.DANGER}
onPress={onRemovePress}
@@ -165,7 +155,7 @@ class BlocklistRow extends Component {
})
}
<BlocklistDetailsModal
<BlacklistDetailsModal
isOpen={this.state.isDetailsModalOpen}
sourceTitle={sourceTitle}
protocol={protocol}
@@ -179,7 +169,7 @@ class BlocklistRow extends Component {
}
BlocklistRow.propTypes = {
BlacklistRow.propTypes = {
id: PropTypes.number.isRequired,
series: PropTypes.object.isRequired,
sourceTitle: PropTypes.string.isRequired,
@@ -189,10 +179,8 @@ BlocklistRow.propTypes = {
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
message: PropTypes.string,
isSelected: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired
};
export default BlocklistRow;
export default BlacklistRow;

View File

@@ -1,8 +1,8 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
import BlocklistRow from './BlocklistRow';
import BlacklistRow from './BlacklistRow';
function createMapStateToProps() {
return createSelector(
@@ -18,9 +18,9 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
onRemovePress() {
dispatch(removeBlocklistItem({ id: props.id }));
dispatch(removeFromBlacklist({ id: props.id }));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);
export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow);

View File

@@ -1,232 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { align, icons, kinds } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import BlocklistRowConnector from './BlocklistRowConnector';
class Blocklist extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isConfirmRemoveModalOpen: false,
items: props.items
};
}
componentDidUpdate(prevProps) {
const {
items
} = this.props;
if (hasDifferentItems(prevProps.items, items)) {
this.setState((state) => {
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
return;
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
}
//
// Listeners
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
}
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true });
}
onRemoveSelectedConfirmed = () => {
this.props.onRemoveSelected(this.getSelectedIds());
this.setState({ isConfirmRemoveModalOpen: false });
}
onConfirmRemoveModalClose = () => {
this.setState({ isConfirmRemoveModalOpen: false });
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
columns,
totalRecords,
isRemoving,
isClearingBlocklistExecuting,
onClearBlocklistPress,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
return (
<PageContent title="Blocklist">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Remove Selected"
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
onPress={this.onRemoveSelectedPress}
/>
<PageToolbarButton
label="Clear"
iconName={icons.CLEAR}
isSpinning={isClearingBlocklistExecuting}
onPress={onClearBlocklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to load blocklist</div>
}
{
isPopulated && !error && !items.length &&
<div>
No history blocklist
</div>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
{...otherProps}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<BlocklistRowConnector
key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
</div>
}
</PageContentBody>
<ConfirmModal
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title="Remove Selected"
message={'Are you sure you want to remove the selected items from the blocklist?'}
confirmLabel="Remove Selected"
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
/>
</PageContent>
);
}
}
Blocklist.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlocklistPress: PropTypes.func.isRequired
};
export default Blocklist;

View File

@@ -1,160 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import withCurrentPage from 'Components/withCurrentPage';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import * as blocklistActions from 'Store/Actions/blocklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import Blocklist from './Blocklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blocklist,
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
(blocklist, isClearingBlocklistExecuting) => {
return {
isClearingBlocklistExecuting,
...blocklist
};
}
);
}
const mapDispatchToProps = {
...blocklistActions,
executeCommand
};
class BlocklistConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchBlocklist,
gotoBlocklistFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchBlocklist();
} else {
gotoBlocklistFirstPage();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
this.props.gotoBlocklistFirstPage();
}
}
componentWillUnmount() {
this.props.clearBlocklist();
unregisterPagePopulator(this.repopulate);
}
//
// Control
repopulate = () => {
this.props.fetchBlocklist();
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoBlocklistFirstPage();
}
onPreviousPagePress = () => {
this.props.gotoBlocklistPreviousPage();
}
onNextPagePress = () => {
this.props.gotoBlocklistNextPage();
}
onLastPagePress = () => {
this.props.gotoBlocklistLastPage();
}
onPageSelect = (page) => {
this.props.gotoBlocklistPage({ page });
}
onRemoveSelected = (ids) => {
this.props.removeBlocklistItems({ ids });
}
onSortPress = (sortKey) => {
this.props.setBlocklistSort({ sortKey });
}
onTableOptionChange = (payload) => {
this.props.setBlocklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlocklistFirstPage();
}
}
onClearBlocklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
}
onTableOptionChange = (payload) => {
this.props.setBlocklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlocklistFirstPage();
}
}
//
// Render
render() {
return (
<Blocklist
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlocklistPress={this.onClearBlocklistPress}
{...this.props}
/>
);
}
}
BlocklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlocklist: PropTypes.func.isRequired,
gotoBlocklistFirstPage: PropTypes.func.isRequired,
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
gotoBlocklistNextPage: PropTypes.func.isRequired,
gotoBlocklistLastPage: PropTypes.func.isRequired,
gotoBlocklistPage: PropTypes.func.isRequired,
removeBlocklistItems: PropTypes.func.isRequired,
setBlocklistSort: PropTypes.func.isRequired,
setBlocklistTableOption: PropTypes.func.isRequired,
clearBlocklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
);

View File

@@ -1,8 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import formatAge from 'Utilities/Number/formatAge';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import formatAge from 'Utilities/Number/formatAge';
import Link from 'Components/Link/Link';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
@@ -23,11 +22,8 @@ function HistoryDetails(props) {
const {
indexer,
releaseGroup,
preferredWordScore,
seriesMatchType,
nzbInfoUrl,
downloadClient,
downloadClientName,
downloadId,
age,
ageHours,
@@ -35,8 +31,6 @@ function HistoryDetails(props) {
publishedDate
} = data;
const downloadClientNameInfo = downloadClientName ?? downloadClient;
return (
<DescriptionList>
<DescriptionListItem
@@ -46,45 +40,24 @@ function HistoryDetails(props) {
/>
{
indexer ?
!!indexer &&
<DescriptionListItem
title="Indexer"
data={indexer}
/> :
null
/>
}
{
releaseGroup ?
!!releaseGroup &&
<DescriptionListItem
descriptionClassName={styles.description}
title="Release Group"
data={releaseGroup}
/> :
null
/>
}
{
preferredWordScore && preferredWordScore !== '0' ?
<DescriptionListItem
title="Preferred Word Score"
data={formatPreferredWordScore(preferredWordScore)}
/> :
null
}
{
seriesMatchType ?
<DescriptionListItem
descriptionClassName={styles.description}
title="Series Match Type"
data={seriesMatchType}
/> :
null
}
{
nzbInfoUrl ?
!!nzbInfoUrl &&
<span>
<DescriptionListItemTitle>
Info URL
@@ -93,44 +66,39 @@ function HistoryDetails(props) {
<DescriptionListItemDescription>
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
</DescriptionListItemDescription>
</span> :
null
</span>
}
{
downloadClientNameInfo ?
!!downloadClient &&
<DescriptionListItem
title="Download Client"
data={downloadClientNameInfo}
/> :
null
data={downloadClient}
/>
}
{
downloadId ?
!!downloadId &&
<DescriptionListItem
title="Grab ID"
data={downloadId}
/> :
null
/>
}
{
age || ageHours || ageMinutes ?
!!indexer &&
<DescriptionListItem
title="Age (when grabbed)"
data={formatAge(age, ageHours, ageMinutes)}
/> :
null
/>
}
{
publishedDate ?
!!publishedDate &&
<DescriptionListItem
title="Published Date"
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
/> :
null
/>
}
</DescriptionList>
);
@@ -150,12 +118,11 @@ function HistoryDetails(props) {
/>
{
message ?
!!message &&
<DescriptionListItem
title="Message"
data={message}
/> :
null
/>
}
</DescriptionList>
);
@@ -163,7 +130,6 @@ function HistoryDetails(props) {
if (eventType === 'downloadFolderImported') {
const {
preferredWordScore,
droppedPath,
importedPath
} = data;
@@ -177,32 +143,21 @@ function HistoryDetails(props) {
/>
{
droppedPath ?
!!droppedPath &&
<DescriptionListItem
descriptionClassName={styles.description}
title="Source"
data={droppedPath}
/> :
null
/>
}
{
importedPath ?
!!importedPath &&
<DescriptionListItem
descriptionClassName={styles.description}
title="Imported To"
data={importedPath}
/> :
null
}
{
preferredWordScore && preferredWordScore !== '0' ?
<DescriptionListItem
title="Preferred Word Score"
data={formatPreferredWordScore(preferredWordScore)}
/> :
null
/>
}
</DescriptionList>
);
@@ -210,8 +165,7 @@ function HistoryDetails(props) {
if (eventType === 'episodeFileDeleted') {
const {
reason,
preferredWordScore
reason
} = data;
let reasonMessage = '';
@@ -221,7 +175,7 @@ function HistoryDetails(props) {
reasonMessage = 'File was deleted by via UI';
break;
case 'MissingFromDisk':
reasonMessage = 'Sonarr was unable to find the file on disk so the file was unlinked from the episode in the database';
reasonMessage = 'Sonarr was unable to find the file on disk so it was removed';
break;
case 'Upgrade':
reasonMessage = 'File was deleted to import an upgrade';
@@ -241,15 +195,6 @@ function HistoryDetails(props) {
title="Reason"
data={reasonMessage}
/>
{
preferredWordScore && preferredWordScore !== '0' ?
<DescriptionListItem
title="Preferred Word Score"
data={formatPreferredWordScore(preferredWordScore)}
/> :
null
}
</DescriptionList>
);
}
@@ -301,12 +246,11 @@ function HistoryDetails(props) {
/>
{
message ?
!!message &&
<DescriptionListItem
title="Message"
data={message}
/> :
null
/>
}
</DescriptionList>
);

View File

@@ -10,12 +10,6 @@
width: 80px;
}
.preferredWordScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 55px;
}
.releaseGroup {
composes: cell from '~Components/Table/Cells/TableRowCell.css';

View File

@@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
@@ -195,17 +194,6 @@ class HistoryRow extends Component {
);
}
if (name === 'preferredWordScore') {
return (
<TableRowCell
key={name}
className={styles.preferredWordScore}
>
{formatPreferredWordScore(data.preferredWordScore)}
</TableRowCell>
);
}
if (name === 'releaseGroup') {
return (
<TableRowCell
@@ -217,16 +205,6 @@ class HistoryRow extends Component {
);
}
if (name === 'sourceTitle') {
return (
<TableRowCell
key={name}
>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'details') {
return (
<TableRowCell

View File

@@ -31,8 +31,6 @@ class Queue extends Component {
constructor(props, context) {
super(props, context);
this._shouldBlockRefresh = false;
this.state = {
allSelected: false,
allUnselected: false,
@@ -44,14 +42,6 @@ class Queue extends Component {
};
}
shouldComponentUpdate() {
if (this._shouldBlockRefresh) {
return false;
}
return true;
}
componentDidUpdate(prevProps) {
const {
items,
@@ -92,10 +82,6 @@ class Queue extends Component {
//
// Listeners
onQueueRowModalOpenOrClose = (isOpen) => {
this._shouldBlockRefresh = isOpen;
}
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
@@ -111,19 +97,15 @@ class Queue extends Component {
}
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true }, () => {
this._shouldBlockRefresh = true;
});
this.setState({ isConfirmRemoveModalOpen: true });
}
onRemoveSelectedConfirmed = (payload) => {
this._shouldBlockRefresh = false;
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
this.setState({ isConfirmRemoveModalOpen: false });
}
onConfirmRemoveModalClose = () => {
this._shouldBlockRefresh = false;
this.setState({ isConfirmRemoveModalOpen: false });
}
@@ -223,7 +205,7 @@ class Queue extends Component {
}
{
isAllPopulated && !hasError && !items.length &&
isPopulated && !hasError && !items.length &&
<div>
Queue is empty
</div>
@@ -252,7 +234,6 @@ class Queue extends Component {
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
/>
);
})
@@ -279,17 +260,6 @@ class Queue extends Component {
return !!(item && item.seriesId && item.episodeId);
})
)}
allPending={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
if (!item) {
return false;
}
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
})
)}
onRemovePress={this.onRemoveSelectedConfirmed}
onModalClose={this.onConfirmRemoveModalClose}
/>

View File

@@ -48,7 +48,6 @@ class QueueConnector extends Component {
const {
useCurrentPage,
fetchQueue,
fetchQueueStatus,
gotoQueueFirstPage
} = this.props;
@@ -59,8 +58,6 @@ class QueueConnector extends Component {
} else {
gotoQueueFirstPage();
}
fetchQueueStatus();
}
componentDidUpdate(prevProps) {
@@ -171,7 +168,6 @@ QueueConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchQueue: PropTypes.func.isRequired,
fetchQueueStatus: PropTypes.func.isRequired,
gotoQueueFirstPage: PropTypes.func.isRequired,
gotoQueuePreviousPage: PropTypes.func.isRequired,
gotoQueueNextPage: PropTypes.func.isRequired,

View File

@@ -19,7 +19,6 @@ import QueueStatusCell from './QueueStatusCell';
import TimeleftCell from './TimeleftCell';
import RemoveQueueItemModal from './RemoveQueueItemModal';
import styles from './QueueRow.css';
import formatBytes from 'Utilities/Number/formatBytes';
class QueueRow extends Component {
@@ -42,33 +41,20 @@ class QueueRow extends Component {
this.setState({ isRemoveQueueItemModalOpen: true });
}
onRemoveQueueItemModalConfirmed = (blocklist) => {
const {
onRemoveQueueItemPress,
onQueueRowModalOpenOrClose
} = this.props;
onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blocklist);
onRemoveQueueItemModalConfirmed = (blacklist) => {
this.props.onRemoveQueueItemPress(blacklist);
this.setState({ isRemoveQueueItemModalOpen: false });
}
onRemoveQueueItemModalClose = () => {
this.props.onQueueRowModalOpenOrClose(false);
this.setState({ isRemoveQueueItemModalOpen: false });
}
onInteractiveImportPress = () => {
this.props.onQueueRowModalOpenOrClose(true);
this.setState({ isInteractiveImportModalOpen: true });
}
onInteractiveImportModalClose = () => {
this.props.onQueueRowModalOpenOrClose(false);
this.setState({ isInteractiveImportModalOpen: false });
}
@@ -281,12 +267,6 @@ class QueueRow extends Component {
);
}
if (name === 'size') {
return (
<TableRowCell key={name}>{formatBytes(size)}</TableRowCell>
);
}
if (name === 'outputPath') {
return (
<TableRowCell key={name}>
@@ -377,7 +357,6 @@ class QueueRow extends Component {
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canIgnore={!!series}
isPending={isPending}
onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose}
/>
@@ -418,8 +397,7 @@ QueueRow.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired,
onRemoveQueueItemPress: PropTypes.func.isRequired,
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
onRemoveQueueItemPress: PropTypes.func.isRequired
};
QueueRow.defaultProps = {

View File

@@ -3,7 +3,3 @@
width: 30px;
}
.noMessages {
margin-bottom: 10px;
}

View File

@@ -12,10 +12,7 @@ function getDetailedPopoverBody(statusMessages) {
{
statusMessages.map(({ title, messages }) => {
return (
<div
key={title}
className={messages.length ? undefined: styles.noMessages}
>
<div key={title}>
{title}
<ul>
{

View File

@@ -21,7 +21,7 @@ class RemoveQueueItemModal extends Component {
this.state = {
remove: true,
blocklist: false
blacklist: false
};
}
@@ -31,7 +31,7 @@ class RemoveQueueItemModal extends Component {
resetState = function() {
this.setState({
remove: true,
blocklist: false
blacklist: false
});
}
@@ -42,8 +42,8 @@ class RemoveQueueItemModal extends Component {
this.setState({ remove: value });
}
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
}
onRemoveConfirmed = () => {
@@ -65,11 +65,10 @@ class RemoveQueueItemModal extends Component {
const {
isOpen,
sourceTitle,
canIgnore,
isPending
canIgnore
} = this.props;
const { remove, blocklist } = this.state;
const { remove, blacklist } = this.state;
return (
<Modal
@@ -89,32 +88,28 @@ class RemoveQueueItemModal extends Component {
Are you sure you want to remove '{sourceTitle}' from the queue?
</div>
{
isPending ?
null :
<FormGroup>
<FormLabel>Remove From Download Client</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>Add Release To Blocklist</FormLabel>
<FormLabel>Remove From Download Client</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Blacklist Release</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
value={blacklist}
helpText="Starts a search for this episode again and prevents this release from being grabbed again"
onChange={this.onBlocklistChange}
onChange={this.onBlacklistChange}
/>
</FormGroup>
@@ -142,7 +137,6 @@ RemoveQueueItemModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
canIgnore: PropTypes.bool.isRequired,
isPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -22,7 +22,7 @@ class RemoveQueueItemsModal extends Component {
this.state = {
remove: true,
blocklist: false
blacklist: false
};
}
@@ -32,7 +32,7 @@ class RemoveQueueItemsModal extends Component {
resetState = function() {
this.setState({
remove: true,
blocklist: false
blacklist: false
});
}
@@ -43,8 +43,8 @@ class RemoveQueueItemsModal extends Component {
this.setState({ remove: value });
}
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
}
onRemoveConfirmed = () => {
@@ -66,11 +66,10 @@ class RemoveQueueItemsModal extends Component {
const {
isOpen,
selectedCount,
canIgnore,
allPending
canIgnore
} = this.props;
const { remove, blocklist } = this.state;
const { remove, blacklist } = this.state;
return (
<Modal
@@ -90,34 +89,30 @@ class RemoveQueueItemsModal extends Component {
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
</div>
{
allPending ?
null :
<FormGroup>
<FormLabel>Remove From Download Client</FormLabel>
<FormGroup>
<FormLabel>Remove From Download Client</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
Add Release{selectedCount > 1 ? 's' : ''} To Blocklist
Blacklist Release{selectedCount > 1 ? 's' : ''}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
name="blacklist"
value={blacklist}
helpText="Prevents Sonarr from automatically grabbing this episode again"
onChange={this.onBlocklistChange}
onChange={this.onBlacklistChange}
/>
</FormGroup>
@@ -145,7 +140,6 @@ RemoveQueueItemsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
selectedCount: PropTypes.number.isRequired,
canIgnore: PropTypes.bool.isRequired,
allPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -154,7 +154,7 @@ class AddNewSeries extends Component {
<div className={styles.noResults}>Couldn't find any results for '{term}'</div>
<div>You can also search using TVDB ID of a show. eg. tvdb:71663</div>
<div>
<Link to="https://wiki.servarr.com/sonarr/faq#why-cant-i-add-a-new-series-when-i-know-the-tvdb-id">
<Link to="https://github.com/Sonarr/Sonarr/wiki/FAQ#why-cant-i-add-a-new-series-when-i-know-the-tvdb-id">
Why can't I find my show?
</Link>
</div>

View File

@@ -30,7 +30,9 @@ class AddNewSeriesModalContent extends Component {
this.state = {
seriesType: props.initialSeriesType === seriesTypes.STANDARD ?
props.seriesType.value :
props.initialSeriesType
props.initialSeriesType,
searchForMissingEpisodes: false,
searchForCutoffUnmetEpisodes: false
};
}
@@ -43,6 +45,14 @@ class AddNewSeriesModalContent extends Component {
//
// Listeners
onSearchForMissingEpisodesChange = ({ value }) => {
this.setState({ searchForMissingEpisodes: value });
}
onSearchForCutoffUnmetEpisodesChange = ({ value }) => {
this.setState({ searchForCutoffUnmetEpisodes: value });
}
onQualityProfileIdChange = ({ value }) => {
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
}
@@ -53,10 +63,14 @@ class AddNewSeriesModalContent extends Component {
onAddSeriesPress = () => {
const {
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes,
seriesType
} = this.state;
this.props.onAddSeriesPress(
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes,
seriesType
);
}
@@ -77,8 +91,6 @@ class AddNewSeriesModalContent extends Component {
languageProfileId,
seriesType,
seasonFolder,
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes,
folder,
tags,
showLanguageProfile,
@@ -89,6 +101,11 @@ class AddNewSeriesModalContent extends Component {
...otherProps
} = this.props;
const {
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
@@ -254,8 +271,8 @@ class AddNewSeriesModalContent extends Component {
containerClassName={styles.searchInputContainer}
className={styles.searchInput}
name="searchForMissingEpisodes"
onChange={onInputChange}
{...searchForMissingEpisodes}
value={searchForMissingEpisodes}
onChange={this.onSearchForMissingEpisodesChange}
/>
</label>
@@ -268,8 +285,8 @@ class AddNewSeriesModalContent extends Component {
containerClassName={styles.searchInputContainer}
className={styles.searchInput}
name="searchForCutoffUnmetEpisodes"
onChange={onInputChange}
{...searchForCutoffUnmetEpisodes}
value={searchForCutoffUnmetEpisodes}
onChange={this.onSearchForCutoffUnmetEpisodesChange}
/>
</label>
</div>
@@ -302,8 +319,6 @@ AddNewSeriesModalContent.propTypes = {
languageProfileId: PropTypes.object,
seriesType: PropTypes.object.isRequired,
seasonFolder: PropTypes.object.isRequired,
searchForMissingEpisodes: PropTypes.object.isRequired,
searchForCutoffUnmetEpisodes: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
tags: PropTypes.object.isRequired,
showLanguageProfile: PropTypes.bool.isRequired,

View File

@@ -55,7 +55,7 @@ class AddNewSeriesModalContentConnector extends Component {
this.props.setAddSeriesDefault({ [name]: value });
}
onAddSeriesPress = (seriesType) => {
onAddSeriesPress = (searchForMissingEpisodes, searchForCutoffUnmetEpisodes, seriesType) => {
const {
tvdbId,
rootFolderPath,
@@ -63,8 +63,6 @@ class AddNewSeriesModalContentConnector extends Component {
qualityProfileId,
languageProfileId,
seasonFolder,
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes,
tags
} = this.props;
@@ -76,9 +74,9 @@ class AddNewSeriesModalContentConnector extends Component {
languageProfileId: languageProfileId.value,
seriesType,
seasonFolder: seasonFolder.value,
searchForMissingEpisodes: searchForMissingEpisodes.value,
searchForCutoffUnmetEpisodes: searchForCutoffUnmetEpisodes.value,
tags: tags.value
tags: tags.value,
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes
});
}
@@ -104,8 +102,6 @@ AddNewSeriesModalContentConnector.propTypes = {
languageProfileId: PropTypes.object,
seriesType: PropTypes.object.isRequired,
seasonFolder: PropTypes.object.isRequired,
searchForMissingEpisodes: PropTypes.object.isRequired,
searchForCutoffUnmetEpisodes: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired,
onModalClose: PropTypes.func.isRequired,
setAddSeriesDefault: PropTypes.func.isRequired,

View File

@@ -1,5 +1,4 @@
.container {
display: flex;
.series {
padding: 10px 20px;
width: 100%;
@@ -7,19 +6,3 @@
background-color: $menuItemHoverBackgroundColor;
}
}
.series {
flex: 1 0 0;
overflow: hidden;
}
.tvdbLink {
composes: link from '~Components/Link/Link.css';
margin-left: auto;
color: $textColor;
}
.tvdbLinkIcon {
margin-left: 10px;
}

View File

@@ -1,28 +1,33 @@
import PropTypes from 'prop-types';
import React, { useCallback } from 'react';
import { icons } from 'Helpers/Props';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
import Icon from 'Components/Icon';
import ImportSeriesTitle from './ImportSeriesTitle';
import styles from './ImportSeriesSearchResult.css';
function ImportSeriesSearchResult(props) {
const {
tvdbId,
title,
year,
network,
isExistingSeries,
onPress
} = props;
class ImportSeriesSearchResult extends Component {
const onPressCallback = useCallback(() => onPress(tvdbId), [tvdbId, onPress]);
//
// Listeners
return (
<div className={styles.container}>
onPress = () => {
this.props.onPress(this.props.tvdbId);
}
//
// Render
render() {
const {
title,
year,
network,
isExistingSeries
} = this.props;
return (
<Link
className={styles.series}
onPress={onPressCallback}
onPress={this.onPress}
>
<ImportSeriesTitle
title={title}
@@ -31,19 +36,8 @@ function ImportSeriesSearchResult(props) {
isExistingSeries={isExistingSeries}
/>
</Link>
<Link
className={styles.tvdbLink}
to={`http://www.thetvdb.com/?tab=series&id=${tvdbId}`}
>
<Icon
className={styles.tvdbLinkIcon}
name={icons.EXTERNAL_LINK}
size={16}
/>
</Link>
</div>
);
);
}
}
ImportSeriesSearchResult.propTypes = {

View File

@@ -20,27 +20,24 @@ function ImportSeriesTitle(props) {
{
!title.contains(year) &&
year > 0 ?
year > 0 &&
<span className={styles.year}>
({year})
</span> :
null
</span>
}
{
network ?
<Label>{network}</Label> :
null
!!network &&
<Label>{network}</Label>
}
{
isExistingSeries ?
isExistingSeries &&
<Label
kind={kinds.WARNING}
>
Existing
</Label> :
null
</Label>
}
</div>
);

View File

@@ -30,9 +30,3 @@
.importButtonIcon {
margin-right: 8px;
}
.addErrorAlert {
composes: alert from '~Components/Alert.css';
margin: 20px 0;
}

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds, sizes } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import FieldSet from 'Components/FieldSet';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -48,27 +47,21 @@ class ImportSeriesSelectFolder extends Component {
isWindows,
isFetching,
isPopulated,
isSaving,
error,
saveError,
items
} = this.props;
const hasRootFolders = items.length > 0;
return (
<PageContent title="Import Series">
<PageContentBody>
{
isFetching && !isPopulated ?
<LoadingIndicator /> :
null
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && error ?
<div>Unable to load root folders</div> :
null
!isFetching && !!error &&
<div>Unable to load root folders</div>
}
{
@@ -85,16 +78,13 @@ class ImportSeriesSelectFolder extends Component {
Make sure that your files include the quality in their filenames. eg. <span className={styles.code}>episode.s02e15.bluray.mkv</span>
</li>
<li className={styles.tip}>
Point Sonarr to the folder containing all of your tv shows, not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\tv shows' : '/tv shows'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\tv shows\\the simpsons' : '/tv shows/the simpsons'}"</span> Additionally, each series must be in its own folder within the root/library folder.
</li>
<li className={styles.tip}>
Do not use for importing downloads from your download client, this is only for existing organized libraries, not unsorted files.
Point Sonarr to the folder containing all of your tv shows, not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\tv shows' : '/tv shows'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\tv shows\\the simpsons' : '/tv shows/the simpsons'}"</span>
</li>
</ul>
</div>
{
hasRootFolders ?
items.length > 0 ?
<div className={styles.recentFolders}>
<FieldSet legend="Root Folders">
<RootFolders
@@ -104,51 +94,35 @@ class ImportSeriesSelectFolder extends Component {
items={items}
/>
</FieldSet>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
Choose another folder
</Button>
</div> :
null
<div className={styles.startImport}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
Start Import
</Button>
</div>
}
{
!isSaving && saveError ?
<Alert
className={styles.addErrorAlert}
kind={kinds.DANGER}
>
Unable to add root folder
<ul>
{
saveError.responseJSON.map((e, index) => {
return (
<li key={index}>
{e.errorMessage}
</li>
);
})
}
</ul>
</Alert> :
null
}
<div className={hasRootFolders ? undefined : styles.startImport}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
{
hasRootFolders ?
'Choose another folder' :
'Start Import'
}
</Button>
</div>
<FileBrowserModal
isOpen={this.state.isAddNewRootFolderModalOpen}
name="rootFolderPath"
@@ -168,9 +142,7 @@ ImportSeriesSelectFolder.propTypes = {
isWindows: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
error: PropTypes.object,
saveError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onNewRootFolderSelect: PropTypes.func.isRequired
};

View File

@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
function App({ store, history }) {
return (
<DocumentTitle title={window.Sonarr.instanceName}>
<DocumentTitle title="Sonarr">
<Provider store={store}>
<ConnectedRouter history={history}>
<PageConnector>

View File

@@ -13,13 +13,13 @@ import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnecto
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import Settings from 'Settings/Settings';
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
import Profiles from 'Settings/Profiles/Profiles';
import QualityConnector from 'Settings/Quality/QualityConnector';
import Quality from 'Settings/Quality/Quality';
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
@@ -118,8 +118,8 @@ function AppRoutes(props) {
/>
<Route
path="/activity/blocklist"
component={BlocklistConnector}
path="/activity/blacklist"
component={BlacklistConnector}
/>
{/*
@@ -158,7 +158,7 @@ function AppRoutes(props) {
<Route
path="/settings/quality"
component={QualityConnector}
component={Quality}
/>
<Route

View File

@@ -27,7 +27,7 @@ function ConnectionLostModal(props) {
<ModalBody>
<div>
Sonarr has lost its connection to the backend and will need to be reloaded to restore functionality.
Sonarr has lost it's connection to the backend and will need to be reloaded to restore functionality.
</div>
<div className={styles.automatic}>

View File

@@ -25,7 +25,7 @@
}
.time {
flex: 0 0 125px;
flex: 0 0 120px;
margin-right: 10px;
border: none !important;
}

View File

@@ -50,7 +50,6 @@ class AgendaEvent extends Component {
absoluteEpisodeNumber,
airDateUtc,
monitored,
unverifiedSceneNumbering,
hasFile,
grabbed,
queueItem,
@@ -71,7 +70,7 @@ class AgendaEvent extends Component {
const statusStyle = getStatusStyle(hasFile, downloading, startTime, endTime, isMonitored);
const missingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber;
const season = series.seasons.find((s) => s.seasonNumber === seasonNumber);
const seasonStatistics = season?.statistics || {};
const seasonStatistics = season.statistics || {};
return (
<div>
@@ -132,16 +131,6 @@ class AgendaEvent extends Component {
/>
}
{
unverifiedSceneNumbering && !missingAbsoluteNumber ?
<Icon
className={styles.statusIcon}
name={icons.WARNING}
title="Scene number hasn't been verified yet"
/> :
null
}
{
!!queueItem &&
<span className={styles.statusIcon}>
@@ -248,7 +237,6 @@ AgendaEvent.propTypes = {
absoluteEpisodeNumber: PropTypes.number,
airDateUtc: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
unverifiedSceneNumbering: PropTypes.bool,
hasFile: PropTypes.bool.isRequired,
grabbed: PropTypes.bool,
queueItem: PropTypes.object,

View File

@@ -1,5 +1,3 @@
$fullColorGradient: rgba(244, 245, 246, 0.2);
.event {
overflow-x: hidden;
margin: 4px 2px;
@@ -38,11 +36,6 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
margin-left: 3px;
}
.statusContainer {
display: flex;
align-items: center;
}
.statusIcon {
margin-left: 3px;
}
@@ -58,10 +51,6 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
.downloaded {
border-left-color: $successColor !important;
&:global(.fullColor) {
background-color: rgba(39, 194, 76, 0.4) !important;
}
&:global(.colorImpaired) {
border-left-color: color($successColor, saturation(+15%)) !important;
}
@@ -69,73 +58,37 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
.downloading {
border-left-color: $purple !important;
&:global(.fullColor) {
background-color: rgba(122, 67, 182, 0.4) !important;
}
}
.unmonitored {
border-left-color: $gray !important;
&:global(.fullColor) {
background-color: rgba(173, 173, 173, 0.5) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(45deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}
.onAir {
border-left-color: $warningColor !important;
&:global(.fullColor) {
background-color: rgba(255, 165, 0, 0.6) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}
.missing {
border-left-color: $dangerColor !important;
&:global(.fullColor) {
background-color: rgba(240, 80, 80, 0.6) !important;
}
&:global(.colorImpaired) {
border-left-color: color($dangerColor saturation(+15%)) !important;
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}
.unaired {
border-left-color: $primaryColor !important;
&:global(.fullColor) {
background-color: rgba(93, 156, 236, 0.4) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}

View File

@@ -1,6 +1,6 @@
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import React, { Component } from 'react';
import classNames from 'classnames';
import { icons, kinds } from 'Helpers/Props';
import formatTime from 'Utilities/Date/formatTime';
@@ -55,7 +55,6 @@ class CalendarEvent extends Component {
absoluteEpisodeNumber,
airDateUtc,
monitored,
unverifiedSceneNumbering,
hasFile,
grabbed,
queueItem,
@@ -63,7 +62,6 @@ class CalendarEvent extends Component {
showFinaleIcon,
showSpecialIcon,
showCutoffUnmetIcon,
fullColorEvents,
timeFormat,
colorImpairedMode
} = this.props;
@@ -79,16 +77,15 @@ class CalendarEvent extends Component {
const statusStyle = getStatusStyle(hasFile, isDownloading, startTime, endTime, isMonitored);
const missingAbsoluteNumber = series.seriesType === 'anime' && seasonNumber > 0 && !absoluteEpisodeNumber;
const season = series.seasons.find((s) => s.seasonNumber === seasonNumber);
const seasonStatistics = season?.statistics || {};
const seasonStatistics = season.statistics || {};
return (
<Fragment>
<div>
<Link
className={classNames(
styles.event,
styles[statusStyle],
colorImpairedMode && 'colorImpaired',
fullColorEvents && 'fullColor'
colorImpairedMode && 'colorImpaired'
)}
component="div"
onPress={this.onPress}
@@ -98,117 +95,95 @@ class CalendarEvent extends Component {
{series.title}
</div>
<div className={styles.statusContainer}>
{
missingAbsoluteNumber ?
<Icon
className={styles.statusIcon}
name={icons.WARNING}
title="Episode does not have an absolute episode number"
/> :
null
}
{
missingAbsoluteNumber &&
<Icon
className={styles.statusIcon}
name={icons.WARNING}
title="Episode does not have an absolute episode number"
/>
}
{
unverifiedSceneNumbering && !missingAbsoluteNumber ?
<Icon
className={styles.statusIcon}
name={icons.WARNING}
title="Scene number hasn't been verified yet"
/> :
null
}
{
!!queueItem &&
<span className={styles.statusIcon}>
<CalendarEventQueueDetails
{...queueItem}
/>
</span>
}
{
queueItem ?
<span className={styles.statusIcon}>
<CalendarEventQueueDetails
{...queueItem}
/>
</span> :
null
}
{
!queueItem && grabbed &&
<Icon
className={styles.statusIcon}
name={icons.DOWNLOADING}
title="Episode is downloading"
/>
}
{
!queueItem && grabbed ?
<Icon
className={styles.statusIcon}
name={icons.DOWNLOADING}
title="Episode is downloading"
/> :
null
}
{
showCutoffUnmetIcon &&
!!episodeFile &&
episodeFile.qualityCutoffNotMet &&
<Icon
className={styles.statusIcon}
name={icons.EPISODE_FILE}
kind={kinds.WARNING}
title="Quality cutoff has not been met"
/>
}
{
showCutoffUnmetIcon &&
!!episodeFile &&
episodeFile.qualityCutoffNotMet ?
<Icon
className={styles.statusIcon}
name={icons.EPISODE_FILE}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
title="Quality cutoff has not been met"
/> :
null
}
{
showCutoffUnmetIcon &&
!!episodeFile &&
episodeFile.languageCutoffNotMet &&
!episodeFile.qualityCutoffNotMet &&
<Icon
className={styles.statusIcon}
name={icons.EPISODE_FILE}
kind={kinds.WARNING}
title="Language cutoff has not been met"
/>
}
{
showCutoffUnmetIcon &&
!!episodeFile &&
episodeFile.languageCutoffNotMet &&
!episodeFile.qualityCutoffNotMet ?
<Icon
className={styles.statusIcon}
name={icons.EPISODE_FILE}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
title="Language cutoff has not been met"
/> :
null
}
{
episodeNumber === 1 && seasonNumber > 0 &&
<Icon
className={styles.statusIcon}
name={icons.INFO}
kind={kinds.INFO}
title={seasonNumber === 1 ? 'Series premiere' : 'Season premiere'}
/>
}
{
episodeNumber === 1 && seasonNumber > 0 ?
<Icon
className={styles.statusIcon}
name={icons.INFO}
kind={kinds.INFO}
darken={fullColorEvents}
title={seasonNumber === 1 ? 'Series premiere' : 'Season premiere'}
/> :
null
}
{
showFinaleIcon &&
episodeNumber !== 1 &&
seasonNumber > 0 &&
episodeNumber === seasonStatistics.totalEpisodeCount &&
<Icon
className={styles.statusIcon}
name={icons.INFO}
kind={kinds.WARNING}
title={series.status === 'ended' ? 'Series finale' : 'Season finale'}
/>
}
{
showFinaleIcon &&
episodeNumber !== 1 &&
seasonNumber > 0 &&
episodeNumber === seasonStatistics.totalEpisodeCount ?
<Icon
className={styles.statusIcon}
name={icons.INFO}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
title={series.status === 'ended' ? 'Series finale' : 'Season finale'}
/> :
null
}
{
showSpecialIcon &&
(episodeNumber === 0 || seasonNumber === 0) ?
<Icon
className={styles.statusIcon}
name={icons.INFO}
kind={kinds.PINK}
darken={fullColorEvents}
title="Special"
/> :
null
}
</div>
{
showSpecialIcon &&
(episodeNumber === 0 || seasonNumber === 0) &&
<Icon
className={styles.statusIcon}
name={icons.INFO}
kind={kinds.PINK}
title="Special"
/>
}
</div>
{
showEpisodeInformation ?
showEpisodeInformation &&
<div className={styles.episodeInfo}>
<div className={styles.episodeTitle}>
{title}
@@ -218,12 +193,11 @@ class CalendarEvent extends Component {
{seasonNumber}x{padNumber(episodeNumber, 2)}
{
series.seriesType === 'anime' && absoluteEpisodeNumber ?
<span className={styles.absoluteEpisodeNumber}>({absoluteEpisodeNumber})</span> : null
series.seriesType === 'anime' && absoluteEpisodeNumber &&
<span className={styles.absoluteEpisodeNumber}>({absoluteEpisodeNumber})</span>
}
</div>
</div> :
null
</div>
}
<div className={styles.airTime}>
@@ -240,7 +214,7 @@ class CalendarEvent extends Component {
showOpenSeriesButton={true}
onModalClose={this.onDetailsModalClose}
/>
</Fragment>
</div>
);
}
}
@@ -255,7 +229,6 @@ CalendarEvent.propTypes = {
absoluteEpisodeNumber: PropTypes.number,
airDateUtc: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
unverifiedSceneNumbering: PropTypes.bool,
hasFile: PropTypes.bool.isRequired,
grabbed: PropTypes.bool,
queueItem: PropTypes.object,
@@ -263,7 +236,6 @@ CalendarEvent.propTypes = {
showFinaleIcon: PropTypes.bool.isRequired,
showSpecialIcon: PropTypes.bool.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired,
colorImpairedMode: PropTypes.bool.isRequired,
onEventModalOpenToggle: PropTypes.func.isRequired

View File

@@ -74,7 +74,6 @@ class CalendarEventGroup extends Component {
showEpisodeInformation,
showFinaleIcon,
timeFormat,
fullColorEvents,
colorImpairedMode,
onEventModalOpenToggle
} = this.props;
@@ -134,8 +133,7 @@ class CalendarEventGroup extends Component {
className={classNames(
styles.eventGroup,
styles[statusStyle],
colorImpairedMode && 'colorImpaired',
fullColorEvents && 'fullColor'
colorImpairedMode && 'colorImpaired'
)}
>
<div className={styles.info}>
@@ -146,7 +144,7 @@ class CalendarEventGroup extends Component {
{
isMissingAbsoluteNumber &&
<Icon
containerClassName={styles.statusIcon}
className={styles.statusIcon}
name={icons.WARNING}
title="Episode does not have an absolute episode number"
/>
@@ -155,7 +153,7 @@ class CalendarEventGroup extends Component {
{
anyDownloading &&
<Icon
containerClassName={styles.statusIcon}
className={styles.statusIcon}
name={icons.DOWNLOADING}
title="An episode is downloading"
/>
@@ -164,10 +162,9 @@ class CalendarEventGroup extends Component {
{
firstEpisode.episodeNumber === 1 && seasonNumber > 0 &&
<Icon
containerClassName={styles.statusIcon}
className={styles.statusIcon}
name={icons.INFO}
kind={kinds.INFO}
darken={fullColorEvents}
title={seasonNumber === 1 ? 'Series Premiere' : 'Season Premiere'}
/>
}
@@ -178,9 +175,9 @@ class CalendarEventGroup extends Component {
seasonNumber > 0 &&
lastEpisode.episodeNumber === series.seasons.find((season) => season.seasonNumber === seasonNumber).statistics.totalEpisodeCount &&
<Icon
containerClassName={styles.statusIcon}
className={styles.statusIcon}
name={icons.INFO}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
kind={kinds.WARNING}
title={series.status === 'ended' ? 'Series finale' : 'Season finale'}
/>
}
@@ -240,7 +237,6 @@ CalendarEventGroup.propTypes = {
isDownloading: PropTypes.bool.isRequired,
showEpisodeInformation: PropTypes.bool.isRequired,
showFinaleIcon: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired,
colorImpairedMode: PropTypes.bool.isRequired,
onEventModalOpenToggle: PropTypes.func.isRequired

View File

@@ -7,23 +7,19 @@ import styles from './Legend.css';
function Legend(props) {
const {
view,
showFinaleIcon,
showSpecialIcon,
showCutoffUnmetIcon,
fullColorEvents,
colorImpairedMode
} = props;
const iconsToShow = [];
const isAgendaView = view === 'agenda';
if (showFinaleIcon) {
iconsToShow.push(
<LegendIconItem
name="Finale"
icon={icons.INFO}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
kind={kinds.WARNING}
tooltip="Series or season finale"
/>
);
@@ -35,7 +31,6 @@ function Legend(props) {
name="Special"
icon={icons.INFO}
kind={kinds.PINK}
darken={fullColorEvents}
tooltip="Special episode"
/>
);
@@ -46,7 +41,7 @@ function Legend(props) {
<LegendIconItem
name="Cutoff Not Met"
icon={icons.EPISODE_FILE}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
kind={kinds.WARNING}
tooltip="Quality or language cutoff has not been met"
/>
);
@@ -58,16 +53,12 @@ function Legend(props) {
<LegendItem
status="unaired"
tooltip="Episode hasn't aired yet"
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
status="unmonitored"
tooltip="Episode is unmonitored"
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
@@ -77,16 +68,12 @@ function Legend(props) {
status="onAir"
name="On Air"
tooltip="Episode is currently airing"
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
status="missing"
tooltip="Episode has aired and is missing from disk"
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
@@ -95,16 +82,12 @@ function Legend(props) {
<LegendItem
status="downloading"
tooltip="Episode is currently downloading"
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
status="downloaded"
tooltip="Episode was downloaded and sorted"
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
@@ -114,7 +97,6 @@ function Legend(props) {
name="Premiere"
icon={icons.INFO}
kind={kinds.INFO}
darken={true}
tooltip="Series or season premiere"
/>
@@ -133,11 +115,9 @@ function Legend(props) {
}
Legend.propTypes = {
view: PropTypes.string.isRequired,
showFinaleIcon: PropTypes.bool.isRequired,
showSpecialIcon: PropTypes.bool.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};

View File

@@ -6,12 +6,10 @@ import Legend from './Legend';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar.options,
(state) => state.calendar.view,
createUISettingsSelector(),
(calendarOptions, view, uiSettings) => {
(calendarOptions, uiSettings) => {
return {
...calendarOptions,
view,
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}

View File

@@ -8,7 +8,6 @@ function LegendIconItem(props) {
name,
icon,
kind,
darken,
tooltip
} = props;
@@ -20,7 +19,6 @@ function LegendIconItem(props) {
<Icon
className={styles.icon}
name={icon}
darken={darken}
kind={kind}
/>
@@ -33,12 +31,7 @@ LegendIconItem.propTypes = {
name: PropTypes.string.isRequired,
icon: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
darken: PropTypes.bool.isRequired,
tooltip: PropTypes.string.isRequired
};
LegendIconItem.defaultProps = {
darken: false
};
export default LegendIconItem;

View File

@@ -9,8 +9,6 @@ function LegendItem(props) {
name,
status,
tooltip,
isAgendaView,
fullColorEvents,
colorImpairedMode
} = props;
@@ -19,8 +17,7 @@ function LegendItem(props) {
className={classNames(
styles.legendItem,
styles[status],
colorImpairedMode && 'colorImpaired',
fullColorEvents && !isAgendaView && 'fullColor'
colorImpairedMode && 'colorImpaired'
)}
title={tooltip}
>
@@ -33,8 +30,6 @@ LegendItem.propTypes = {
name: PropTypes.string,
status: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired,
isAgendaView: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};

View File

@@ -25,16 +25,14 @@ class CalendarOptionsModalContent extends Component {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode,
fullColorEvents
enableColorImpairedMode
} = props;
this.state = {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode,
fullColorEvents
enableColorImpairedMode
};
}
@@ -98,7 +96,6 @@ class CalendarOptionsModalContent extends Component {
showFinaleIcon,
showSpecialIcon,
showCutoffUnmetIcon,
fullColorEvents,
onModalClose
} = this.props;
@@ -177,18 +174,6 @@ class CalendarOptionsModalContent extends Component {
onChange={this.onOptionInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Full Color Events</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="fullColorEvents"
value={fullColorEvents}
helpText="Altered style to color the entire event with the status color, instead of just the left edge. Does not apply to Agenda"
onChange={this.onOptionInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
@@ -229,9 +214,7 @@ class CalendarOptionsModalContent extends Component {
value={timeFormat}
onChange={this.onGlobalInputChange}
/>
</FormGroup>
<FormGroup>
</FormGroup><FormGroup>
<FormLabel>Enable Color-Impaired Mode</FormLabel>
<FormInputGroup
@@ -242,6 +225,7 @@ class CalendarOptionsModalContent extends Component {
onChange={this.onGlobalInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
</ModalBody>
@@ -266,7 +250,6 @@ CalendarOptionsModalContent.propTypes = {
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

View File

@@ -1,7 +1,7 @@
export const APPLICATION_UPDATE = 'ApplicationUpdate';
export const BACKUP = 'Backup';
export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads';
export const CLEAR_BLOCKLIST = 'ClearBlocklist';
export const CLEAR_BLACKLIST = 'ClearBlacklist';
export const CLEAR_LOGS = 'ClearLog';
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';
export const DELETE_LOG_FILES = 'DeleteLogFiles';
@@ -15,7 +15,6 @@ export const REFRESH_SERIES = 'RefreshSeries';
export const RENAME_FILES = 'RenameFiles';
export const RENAME_SERIES = 'RenameSeries';
export const RESET_API_KEY = 'ResetApiKey';
export const RESET_QUALITY_DEFINITIONS = 'ResetQualityDefinitions';
export const RSS_SYNC = 'RssSync';
export const SEASON_SEARCH = 'SeasonSearch';
export const SERIES_SEARCH = 'SeriesSearch';

View File

@@ -16,9 +16,4 @@
color: #3a3f51;
font-size: 21px;
line-height: inherit;
&.small {
color: #909293;
font-size: 18px;
}
}

View File

@@ -1,7 +1,5 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import { sizes } from 'Helpers/Props';
import styles from './FieldSet.css';
class FieldSet extends Component {
@@ -11,14 +9,13 @@ class FieldSet extends Component {
render() {
const {
size,
legend,
children
} = this.props;
return (
<fieldset className={styles.fieldSet}>
<legend className={classNames(styles.legend, (size === sizes.SMALL) && styles.small)}>
<legend className={styles.legend}>
{legend}
</legend>
{children}
@@ -29,13 +26,8 @@ class FieldSet extends Component {
}
FieldSet.propTypes = {
size: PropTypes.oneOf(sizes.all).isRequired,
legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
children: PropTypes.node
};
FieldSet.defaultProps = {
size: sizes.MEDIUM
};
export default FieldSet;

View File

@@ -128,7 +128,7 @@ class FileBrowserModalContent extends Component {
className={styles.mappedDrivesWarning}
kind={kinds.WARNING}
>
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server">FAQ</Link> for more information.
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://github.com/Sonarr/Sonarr/wiki/FAQ">FAQ</Link> for more information.
</Alert>
}

View File

@@ -160,7 +160,6 @@ class DateFilterBuilderRowValue extends Component {
<TextInput
name={NAME}
value={filterValue}
type="date"
placeholder="yyyy-mm-dd"
onChange={this.onValueChange}
/>

View File

@@ -12,7 +12,6 @@ import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue';
import SeriesTypeFilterBuilderRowValue from './SeriesTypeFilterBuilderRowValue';
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
import styles from './FilterBuilderRow.css';
@@ -76,9 +75,6 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.SERIES_STATUS:
return SeriesStatusFilterBuilderRowValue;
case filterBuilderValueTypes.SERIES_TYPES:
return SeriesTypeFilterBuilderRowValue;
case filterBuilderValueTypes.TAG:
return TagFilterBuilderRowValueConnector;

View File

@@ -16,7 +16,7 @@ function createTagListSelector() {
(selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ||
selectedFilterBuilderProp.type === filterBuilderTypes.STRING) &&
filterType !== filterTypes.EQUAL &&
filterType !== filterTypes.NOT_EQUAL ||
filterType !== filterBuilderTypes.NOT_EQUAL ||
!selectedFilterBuilderProp.optionsSelector
) {
return [];

View File

@@ -1,5 +1,5 @@
.tag {
display: flex;
height: 21px;
&.isLastTag {
.or {
@@ -18,5 +18,4 @@
.or {
margin: 0 3px;
color: $themeDarkColor;
line-height: 31px;
}

View File

@@ -6,7 +6,7 @@ import styles from './FilterBuilderRowValueTag.css';
function FilterBuilderRowValueTag(props) {
return (
<div
<span
className={styles.tag}
>
<TagInputTag
@@ -15,13 +15,12 @@ function FilterBuilderRowValueTag(props) {
/>
{
props.isLastTag ?
null :
<div className={styles.or}>
!props.isLastTag &&
<span className={styles.or}>
or
</div>
</span>
}
</div>
</span>
);
}

View File

@@ -1,19 +0,0 @@
import React from 'react';
import FilterBuilderRowValue from './FilterBuilderRowValue';
const seriesTypeList = [
{ id: 'anime', name: 'Anime' },
{ id: 'daily', name: 'Daily' },
{ id: 'standard', name: 'Standard' }
];
function SeriesTypeFilterBuilderRowValue(props) {
return (
<FilterBuilderRowValue
tagList={seriesTypeList}
{...props}
/>
);
}
export default SeriesTypeFilterBuilderRowValue;

View File

@@ -1,100 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
import sortByName from 'Utilities/Array/sortByName';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.downloadClients,
(state, { includeAny }) => includeAny,
(state, { protocol }) => protocol,
(downloadClients, includeAny, protocolFilter) => {
const {
isFetching,
isPopulated,
error,
items
} = downloadClients;
const filteredItems = items.filter((item) => item.protocol === protocolFilter);
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
return {
key: downloadClient.id,
value: downloadClient.name
};
});
if (includeAny) {
values.unshift({
key: 0,
value: '(Any)'
});
}
return {
isFetching,
isPopulated,
error,
values
};
}
);
}
const mapDispatchToProps = {
dispatchFetchDownloadClients: fetchDownloadClients
};
class DownloadClientSelectInputConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isPopulated) {
this.props.dispatchFetchDownloadClients();
}
}
//
// Listeners
onChange = ({ name, value }) => {
this.props.onChange({ name, value: parseInt(value) });
}
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
DownloadClientSelectInputConnector.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeAny: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
dispatchFetchDownloadClients: PropTypes.func.isRequired
};
DownloadClientSelectInputConnector.defaultProps = {
includeAny: false,
protocol: 'torrent'
};
export default connect(createMapStateToProps, mapDispatchToProps)(DownloadClientSelectInputConnector);

View File

@@ -5,10 +5,6 @@
align-items: center;
}
.editableContainer {
width: 100%;
}
.hasError {
composes: hasError from '~Components/Form/Input.css';
}
@@ -26,16 +22,6 @@
margin-left: 12px;
}
.dropdownArrowContainerEditable {
position: absolute;
top: 0;
right: 0;
padding-right: 17px;
width: 30%;
height: 35px;
text-align: right;
}
.dropdownArrowContainerDisabled {
composes: dropdownArrowContainer;
@@ -85,21 +71,3 @@
display: inline-block;
margin: 5px -5px 5px 0;
}
.mobileCloseButtonContainer {
display: flex;
justify-content: flex-end;
height: 40px;
border-bottom: 1px solid $borderColor;
}
.mobileCloseButton {
width: 40px;
height: 40px;
text-align: center;
line-height: 40px;
&:hover {
color: $modalCloseButtonHoverColor;
}
}

View File

@@ -4,7 +4,7 @@ import React, { Component } from 'react';
import { Manager, Popper, Reference } from 'react-popper';
import classNames from 'classnames';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { isMobile as isMobileUtil } from 'Utilities/browser';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import { icons, sizes, scrollDirections } from 'Helpers/Props';
import Icon from 'Components/Icon';
@@ -15,7 +15,6 @@ import Measure from 'Components/Measure';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import Scroller from 'Components/Scroller/Scroller';
import TextInput from './TextInput';
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import HintedSelectInputOption from './HintedSelectInputOption';
import styles from './EnhancedSelectInput.css';
@@ -170,21 +169,11 @@ class EnhancedSelectInput extends Component {
}
}
onFocus = () => {
if (this.state.isOpen) {
this._removeListener();
this.setState({ isOpen: false });
}
}
onBlur = () => {
if (!this.props.isEditable) {
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
const origIndex = getSelectedIndex(this.props);
if (origIndex !== this.state.selectedIndex) {
this.setState({ selectedIndex: origIndex });
}
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
const origIndex = getSelectedIndex(this.props);
if (origIndex !== this.state.selectedIndex) {
this.setState({ selectedIndex: origIndex });
}
}
@@ -308,19 +297,16 @@ class EnhancedSelectInput extends Component {
const {
className,
disabledClassName,
name,
value,
values,
isDisabled,
isEditable,
isFetching,
hasError,
hasWarning,
valueOptions,
selectedValueOptions,
selectedValueComponent: SelectedValueComponent,
optionComponent: OptionComponent,
onChange
optionComponent: OptionComponent
} = this.props;
const {
@@ -346,94 +332,52 @@ class EnhancedSelectInput extends Component {
whitelist={['width']}
onMeasure={this.onMeasure}
>
{
isEditable ?
<div
className={styles.editableContainer}
>
<TextInput
className={className}
name={name}
value={value}
readOnly={isDisabled}
hasError={hasError}
hasWarning={hasWarning}
onFocus={this.onFocus}
onBlur={this.onBlur}
onChange={onChange}
/>
<Link
className={classNames(
styles.dropdownArrowContainerEditable,
isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer)
}
onPress={this.onPress}
>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
<Link
className={classNames(
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
isDisabled && disabledClassName
)}
isDisabled={isDisabled}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
onPress={this.onPress}
>
<SelectedValueComponent
value={value}
values={values}
{...selectedValueOptions}
{...selectedOption}
isDisabled={isDisabled}
isMultiSelect={isMultiSelect}
>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
{
!isFetching &&
<Icon
name={icons.CARET_DOWN}
/>
}
</Link>
</div> :
<Link
className={classNames(
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
isDisabled && disabledClassName
)}
isDisabled={isDisabled}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
onPress={this.onPress}
>
<SelectedValueComponent
value={value}
values={values}
{...selectedValueOptions}
{...selectedOption}
isDisabled={isDisabled}
isMultiSelect={isMultiSelect}
>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
<div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
}
>
<div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
}
>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
{
!isFetching &&
<Icon
name={icons.CARET_DOWN}
/>
}
</div>
</Link>
}
{
!isFetching &&
<Icon
name={icons.CARET_DOWN}
/>
}
</div>
</Link>
</Measure>
</div>
)}
@@ -518,18 +462,6 @@ class EnhancedSelectInput extends Component {
scrollDirection={scrollDirections.NONE}
>
<Scroller className={styles.optionsModalScroller}>
<div className={styles.mobileCloseButtonContainer}>
<Link
className={styles.mobileCloseButton}
onPress={this.onOptionsModalClose}
>
<Icon
name={icons.CLOSE}
size={18}
/>
</Link>
</div>
{
values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
@@ -570,7 +502,6 @@ EnhancedSelectInput.propTypes = {
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
isEditable: PropTypes.bool.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
valueOptions: PropTypes.object.isRequired,
@@ -586,7 +517,6 @@ EnhancedSelectInput.defaultProps = {
disabledClassName: styles.isDisabled,
isDisabled: false,
isFetching: false,
isEditable: false,
valueOptions: {},
selectedValueOptions: {},
selectedValueComponent: HintedSelectInputSelectedValue,

View File

@@ -54,8 +54,4 @@
&:last-child {
border: none;
}
&:hover {
background-color: unset;
}
}

View File

@@ -12,9 +12,7 @@ class EnhancedSelectInputOption extends Component {
//
// Listeners
onPress = (e) => {
e.preventDefault();
onPress = () => {
const {
id,
onSelect

View File

@@ -6,7 +6,6 @@ import AutoCompleteInput from './AutoCompleteInput';
import CaptchaInputConnector from './CaptchaInputConnector';
import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector';
import DownloadClientSelectInputConnector from './DownloadClientSelectInputConnector';
import KeyValueListInput from './KeyValueListInput';
import MonitorEpisodesSelectInput from './MonitorEpisodesSelectInput';
import NumberInput from './NumberInput';
@@ -21,10 +20,8 @@ import SeriesTypeSelectInput from './SeriesTypeSelectInput';
import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import TagInputConnector from './TagInputConnector';
import TagSelectInputConnector from './TagSelectInputConnector';
import TextTagInputConnector from './TextTagInputConnector';
import TextInput from './TextInput';
import UMaskInput from './UMaskInput';
import FormInputHelpText from './FormInputHelpText';
import styles from './FormInputGroup.css';
@@ -69,9 +66,6 @@ function getComponent(type) {
case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector;
case inputTypes.DOWNLOAD_CLIENT_SELECT:
return DownloadClientSelectInputConnector;
case inputTypes.ROOT_FOLDER_SELECT:
return RootFolderSelectInputConnector;
@@ -90,12 +84,6 @@ function getComponent(type) {
case inputTypes.TEXT_TAG:
return TextTagInputConnector;
case inputTypes.TAG_SELECT:
return TagSelectInputConnector;
case inputTypes.UMASK:
return UMaskInput;
default:
return TextInput;
}
@@ -203,7 +191,7 @@ function FormInputGroup(props) {
}
{
(!checkInput || helpText) && helpTextWarning &&
!checkInput && helpTextWarning &&
<FormInputHelpText
text={helpTextWarning}
isWarning={true}

View File

@@ -8,13 +8,8 @@
}
}
.keyInputWrapper {
flex: 6 0 0;
}
.valueInputWrapper {
.inputWrapper {
flex: 1 0 0;
min-width: 40px;
}
.buttonWrapper {

View File

@@ -63,7 +63,7 @@ class KeyValueListInputItem extends Component {
return (
<div className={styles.itemContainer}>
<div className={styles.keyInputWrapper}>
<div className={styles.inputWrapper}>
<TextInput
className={styles.keyInput}
name="key"
@@ -75,7 +75,7 @@ class KeyValueListInputItem extends Component {
/>
</div>
<div className={styles.valueInputWrapper}>
<div className={styles.inputWrapper}>
<TextInput
className={styles.valueInput}
name="value"

View File

@@ -3,17 +3,10 @@ import React from 'react';
import TextInput from './TextInput';
import styles from './PasswordInput.css';
// Prevent a user from copying (or cutting) the password from the input
function onCopy(e) {
e.preventDefault();
e.nativeEvent.stopImmediatePropagation();
}
function PasswordInput(props) {
return (
<TextInput
{...props}
onCopy={onCopy}
/>
);
}

View File

@@ -29,8 +29,6 @@ function getType({ type, selectOptionsProviderAction }) {
return inputTypes.SELECT;
case 'tag':
return inputTypes.TEXT_TAG;
case 'tagSelect':
return inputTypes.TAG_SELECT;
case 'textbox':
return inputTypes.TEXT;
case 'oAuth':

View File

@@ -10,16 +10,13 @@ const ADD_NEW_KEY = 'addNew';
function createMapStateToProps() {
return createSelector(
(state) => state.rootFolders,
(state, { value }) => value,
(state, { includeMissingValue }) => includeMissingValue,
(state, { includeNoChange }) => includeNoChange,
(rootFolders, value, includeMissingValue, includeNoChange) => {
(rootFolders, includeNoChange) => {
const values = rootFolders.items.map((rootFolder) => {
return {
key: rootFolder.path,
value: rootFolder.path,
freeSpace: rootFolder.freeSpace,
isMissing: false
freeSpace: rootFolder.freeSpace
};
});
@@ -27,8 +24,7 @@ function createMapStateToProps() {
values.unshift({
key: 'noChange',
value: 'No Change',
isDisabled: true,
isMissing: false
isDisabled: true
});
}
@@ -41,15 +37,6 @@ function createMapStateToProps() {
});
}
if (includeMissingValue && !values.find((v) => v.key === value)) {
values.push({
key: value,
value,
isMissing: true,
isDisabled: true
});
}
values.push({
key: ADD_NEW_KEY,
value: 'Add a new path'

View File

@@ -27,9 +27,3 @@
color: $darkGray;
font-size: $smallFontSize;
}
.isMissing {
margin-left: 15px;
color: $dangerColor;
font-size: $smallFontSize;
}

View File

@@ -10,7 +10,6 @@ function RootFolderSelectInputOption(props) {
id,
value,
freeSpace,
isMissing,
seriesFolder,
isMobile,
isWindows,
@@ -44,20 +43,11 @@ function RootFolderSelectInputOption(props) {
</div>
{
freeSpace == null ?
null :
freeSpace != null &&
<div className={styles.freeSpace}>
{formatBytes(freeSpace)} Free
</div>
}
{
isMissing ?
<div className={styles.isMissing}>
Missing
</div> :
null
}
</div>
</EnhancedSelectInputOption>
);
@@ -67,7 +57,6 @@ RootFolderSelectInputOption.propTypes = {
id: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
freeSpace: PropTypes.number,
isMissing: PropTypes.boolean,
seriesFolder: PropTypes.string,
isMobile: PropTypes.bool.isRequired,
isWindows: PropTypes.bool

Some files were not shown because too many files have changed in this diff Show More