mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-20 16:44:18 -04:00
Compare commits
1 Commits
phantom-rt
...
phantom-ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8502f523e6 |
41
.github/ISSUE_TEMPLATE.md
vendored
Normal file
41
.github/ISSUE_TEMPLATE.md
vendored
Normal 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
|
||||
48
.github/ISSUE_TEMPLATE/bug_report.md
vendored
48
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,36 +1,28 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: 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!
|
||||
title: ''
|
||||
labels: 'bug'
|
||||
assignees: ''
|
||||
name: Bug report
|
||||
about: Create a report to help us improve Sonarr
|
||||
|
||||
---
|
||||
<!-- 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! -->
|
||||
|
||||
**Describe the bug**
|
||||
<!-- A clear and concise description of what the bug is. -->
|
||||
|
||||
**To Reproduce**
|
||||
<!-- Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error -->
|
||||
|
||||
**Expected behavior**
|
||||
<!-- A clear and concise description of what you expected to happen.-->
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Screenshots**
|
||||
<!-- If applicable, add screenshots to help explain your problem.-->
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Platform Information (please complete the following information):**
|
||||
- OS: <!-- [e.g. Windows 10 2004 / Ubuntu 20.04] -->
|
||||
- Docker: <!-- [Yes/No] -->
|
||||
- .net Framework (Windows) or mono (macOS/Linux) (System -> Status): <!--[e.g. Mono 5.8, Mono 6.2, .net 4.5] -->
|
||||
- Browser and Version (Only needed for UI issues): <!--[e.g. chrome 86.0.4240.198] -->
|
||||
- Sonarr Version: <!--[e.g. 2.0.0.5344 , 3.0.4.1077]-->
|
||||
- Sonarr Branch: <!--[e.g. master, develop , phantom-develop]-->
|
||||
**Logs**
|
||||
Link to debug or trace log files.
|
||||
|
||||
**Trace Logs**
|
||||
Turn on Trace logs under Settings -> General and wait for the bug to occur again.
|
||||
**Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!**
|
||||
**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.
|
||||
|
||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
14
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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: http://webchat.freenode.net/?channels=#sonarr
|
||||
about: Chat with users and devs on support and setup related topics.
|
||||
7
.github/ISSUE_TEMPLATE/other-issues.md
vendored
Normal file
7
.github/ISSUE_TEMPLATE/other-issues.md
vendored
Normal 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.
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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
6
.github/SUPPORT.md
vendored
@@ -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 ](http://webchat.freenode.net/?channels=#sonarr)for support/questions.
|
||||
If you have a support question, please use the [support forums](https://forums.sonarr.tv/).
|
||||
|
||||
21
.github/workflows/lock.yml
vendored
21
.github/workflows/lock.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -141,4 +141,3 @@ output/*
|
||||
_start
|
||||
|
||||
src/.idea/
|
||||
/distribution/windows/setup/output/*
|
||||
|
||||
@@ -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://help.github.com/articles/working-with-repositories)
|
||||
3. Install the required Node Packages `yarn install`
|
||||
4. Start gulp to monitor your dev environment for any 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 (currently phantom-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](http://webchat.freenode.net/?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 phantom-develop), never master, 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)
|
||||
|
||||
61
README.md
61
README.md
@@ -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 ](http://webchat.freenode.net/?channels=#sonarr)
|
||||
- [Reddit](https://www.reddit.com/r/sonarr)
|
||||
- [Wiki](https://wiki.servarr.com/Sonarr)
|
||||
|
||||
|
||||
|
||||
## Features
|
||||
|
||||
@@ -38,16 +32,43 @@ 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).
|
||||
<a href="https://github.com/Sonarr/Sonarr/graphs/contributors"><img src="https://opencollective.com/Sonarr/contributors.svg?width=890&button=false" /></a>
|
||||
### Requirements
|
||||
|
||||
- [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!
|
||||
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
|
||||
|
||||
@@ -69,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
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -37,7 +37,6 @@ Compression=lzma2/normal
|
||||
AppContact={#ForumsURL}
|
||||
VersionInfoVersion={#BuildNumber}
|
||||
SetupLogging=yes
|
||||
OutputDir=output
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
@@ -60,7 +59,6 @@ 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"; Flags: runhidden waituntilterminated;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@ gulp.task('build',
|
||||
'webpack',
|
||||
'copyHtml',
|
||||
'copyFonts',
|
||||
'copyImages',
|
||||
'copyRobots'
|
||||
'copyImages'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -32,11 +32,3 @@ gulp.task('copyImages', () => {
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
gulp.task('copyRobots', () => {
|
||||
return gulp.src(paths.src.robots, { base: paths.src.root })
|
||||
.pipe(cache('copyRobots'))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ const paths = {
|
||||
content: `${root}/Content/`,
|
||||
fonts: `${root}/Content/Fonts/`,
|
||||
images: `${root}/Content/Images/`,
|
||||
robots: `${root}/Content/robots.txt`,
|
||||
exclude: {
|
||||
libs: `!${root}/JsLibraries/**`
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ function watch() {
|
||||
gulpWatch(paths.src.html, gulp.series('copyHtml'));
|
||||
gulpWatch(`${paths.src.fonts}**/*.*`, gulp.series('copyFonts'));
|
||||
gulpWatch(`${paths.src.images}**/*.*`, gulp.series('copyImages'));
|
||||
gulpWatch(paths.src.robots, gulp.series('copyRobots'));
|
||||
}
|
||||
|
||||
gulp.task('watch', gulp.series('build', watch));
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
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 { align, icons } 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';
|
||||
@@ -22,72 +15,6 @@ import BlacklistRowConnector from './BlacklistRowConnector';
|
||||
|
||||
class Blacklist 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
|
||||
|
||||
@@ -99,33 +26,15 @@ class Blacklist extends Component {
|
||||
items,
|
||||
columns,
|
||||
totalRecords,
|
||||
isRemoving,
|
||||
isClearingBlacklistExecuting,
|
||||
onClearBlacklistPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmRemoveModalOpen
|
||||
} = this.state;
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title="Blacklist">
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Remove Selected"
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={!selectedIds.length}
|
||||
isSpinning={isRemoving}
|
||||
onPress={this.onRemoveSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Clear"
|
||||
iconName={icons.CLEAR}
|
||||
@@ -150,67 +59,51 @@ class Blacklist extends Component {
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to load blacklist</div>
|
||||
<div>Unable to load blacklist</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<div>
|
||||
No history blacklist
|
||||
</div>
|
||||
<div>
|
||||
No history blacklist
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<BlacklistRowConnector
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id] || false}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<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>
|
||||
<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 blacklist?'}
|
||||
confirmLabel="Remove Selected"
|
||||
onConfirm={this.onRemoveSelectedConfirmed}
|
||||
onCancel={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@@ -223,9 +116,7 @@ Blacklist.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||
onRemoveSelected: PropTypes.func.isRequired,
|
||||
onClearBlacklistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -89,10 +89,6 @@ class BlacklistConnector extends Component {
|
||||
this.props.gotoBlacklistPage({ page });
|
||||
}
|
||||
|
||||
onRemoveSelected = (ids) => {
|
||||
this.props.removeBlacklistItems({ ids });
|
||||
}
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setBlacklistSort({ sortKey });
|
||||
}
|
||||
@@ -128,7 +124,6 @@ class BlacklistConnector extends Component {
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onRemoveSelected={this.onRemoveSelected}
|
||||
onSortPress={this.onSortPress}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onClearBlacklistPress={this.onClearBlacklistPress}
|
||||
@@ -148,7 +143,6 @@ BlacklistConnector.propTypes = {
|
||||
gotoBlacklistNextPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistLastPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistPage: PropTypes.func.isRequired,
|
||||
removeBlacklistItems: PropTypes.func.isRequired,
|
||||
setBlacklistSort: PropTypes.func.isRequired,
|
||||
setBlacklistTableOption: PropTypes.func.isRequired,
|
||||
clearBlacklist: PropTypes.func.isRequired,
|
||||
|
||||
@@ -2,7 +2,6 @@ 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';
|
||||
@@ -41,7 +40,6 @@ class BlacklistRow extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
series,
|
||||
sourceTitle,
|
||||
language,
|
||||
@@ -50,20 +48,12 @@ class BlacklistRow extends Component {
|
||||
protocol,
|
||||
indexer,
|
||||
message,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
onRemovePress
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
@@ -189,9 +179,7 @@ BlacklistRow.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
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
|
||||
import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
|
||||
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
||||
import BlacklistRow from './BlacklistRow';
|
||||
|
||||
@@ -18,7 +18,7 @@ function createMapStateToProps() {
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRemovePress() {
|
||||
dispatch(removeBlacklistItem({ id: props.id }));
|
||||
dispatch(removeFromBlacklist({ id: props.id }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@ class Queue extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._shouldBlockRefresh = false;
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
@@ -44,18 +42,6 @@ class Queue extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (!this._shouldBlockRefresh) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasDifferentItems(this.props.items, nextProps.items)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
@@ -96,10 +82,6 @@ class Queue extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQueueRowModalOpenOrClose = (isOpen) => {
|
||||
this._shouldBlockRefresh = isOpen;
|
||||
}
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
@@ -115,19 +97,16 @@ class Queue extends Component {
|
||||
}
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this._shouldBlockRefresh = true;
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
}
|
||||
|
||||
onRemoveSelectedConfirmed = (payload) => {
|
||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
this._shouldBlockRefresh = false;
|
||||
}
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
this._shouldBlockRefresh = false;
|
||||
}
|
||||
|
||||
//
|
||||
@@ -226,7 +205,7 @@ class Queue extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !hasError && !items.length &&
|
||||
isPopulated && !hasError && !items.length &&
|
||||
<div>
|
||||
Queue is empty
|
||||
</div>
|
||||
@@ -255,7 +234,6 @@ class Queue extends Component {
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -42,32 +42,19 @@ class QueueRow extends Component {
|
||||
}
|
||||
|
||||
onRemoveQueueItemModalConfirmed = (blacklist) => {
|
||||
const {
|
||||
onRemoveQueueItemPress,
|
||||
onQueueRowModalOpenOrClose
|
||||
} = this.props;
|
||||
|
||||
onQueueRowModalOpenOrClose(false);
|
||||
onRemoveQueueItemPress(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 });
|
||||
}
|
||||
|
||||
@@ -410,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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -78,10 +78,7 @@ 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>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
}
|
||||
|
||||
.time {
|
||||
flex: 0 0 125px;
|
||||
flex: 0 0 120px;
|
||||
margin-right: 10px;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.tag {
|
||||
display: flex;
|
||||
height: 21px;
|
||||
|
||||
&.isLastTag {
|
||||
.or {
|
||||
@@ -18,5 +18,4 @@
|
||||
.or {
|
||||
margin: 0 3px;
|
||||
color: $themeDarkColor;
|
||||
line-height: 31px;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
@@ -558,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,
|
||||
@@ -574,7 +517,6 @@ EnhancedSelectInput.defaultProps = {
|
||||
disabledClassName: styles.isDisabled,
|
||||
isDisabled: false,
|
||||
isFetching: false,
|
||||
isEditable: false,
|
||||
valueOptions: {},
|
||||
selectedValueOptions: {},
|
||||
selectedValueComponent: HintedSelectInputSelectedValue,
|
||||
|
||||
@@ -20,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';
|
||||
|
||||
@@ -86,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;
|
||||
}
|
||||
@@ -199,7 +191,7 @@ function FormInputGroup(props) {
|
||||
}
|
||||
|
||||
{
|
||||
(!checkInput || helpText) && helpTextWarning &&
|
||||
!checkInput && helpTextWarning &&
|
||||
<FormInputHelpText
|
||||
text={helpTextWarning}
|
||||
isWarning={true}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -75,12 +75,6 @@ class TagInput extends Component {
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTagEdit = ({ value, ...otherProps }) => {
|
||||
this.setState({ value });
|
||||
|
||||
this.props.onTagDelete(otherProps);
|
||||
}
|
||||
|
||||
onInputContainerPress = () => {
|
||||
this._autosuggestRef.input.focus();
|
||||
}
|
||||
@@ -194,7 +188,6 @@ class TagInput extends Component {
|
||||
const {
|
||||
tags,
|
||||
kind,
|
||||
canEdit,
|
||||
tagComponent,
|
||||
onTagDelete
|
||||
} = this.props;
|
||||
@@ -206,10 +199,8 @@ class TagInput extends Component {
|
||||
kind={kind}
|
||||
inputProps={inputProps}
|
||||
isFocused={this.state.isFocused}
|
||||
canEdit={canEdit}
|
||||
tagComponent={tagComponent}
|
||||
onTagDelete={onTagDelete}
|
||||
onTagEdit={this.onTagEdit}
|
||||
onInputContainerPress={this.onInputContainerPress}
|
||||
/>
|
||||
);
|
||||
@@ -271,7 +262,6 @@ TagInput.propTypes = {
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
delimiters: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
minQueryLength: PropTypes.number.isRequired,
|
||||
canEdit: PropTypes.bool,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
tagComponent: PropTypes.elementType.isRequired,
|
||||
@@ -287,7 +277,6 @@ TagInput.defaultProps = {
|
||||
placeholder: '',
|
||||
delimiters: ['Tab', 'Enter', ' ', ','],
|
||||
minQueryLength: 1,
|
||||
canEdit: false,
|
||||
tagComponent: TagInputTag
|
||||
};
|
||||
|
||||
|
||||
@@ -28,10 +28,8 @@ class TagInputInput extends Component {
|
||||
tags,
|
||||
inputProps,
|
||||
kind,
|
||||
canEdit,
|
||||
tagComponent: TagComponent,
|
||||
onTagDelete,
|
||||
onTagEdit
|
||||
onTagDelete
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -49,10 +47,8 @@ class TagInputInput extends Component {
|
||||
index={index}
|
||||
tag={tag}
|
||||
kind={kind}
|
||||
canEdit={canEdit}
|
||||
isLastTag={index === tags.length - 1}
|
||||
onDelete={onTagDelete}
|
||||
onEdit={onTagEdit}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@@ -71,10 +67,8 @@ TagInputInput.propTypes = {
|
||||
inputProps: PropTypes.object.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
isFocused: PropTypes.bool.isRequired,
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
tagComponent: PropTypes.elementType.isRequired,
|
||||
onTagDelete: PropTypes.func.isRequired,
|
||||
onTagEdit: PropTypes.func.isRequired,
|
||||
onInputContainerPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -1,19 +1,5 @@
|
||||
.tag {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
height: 31px;
|
||||
}
|
||||
|
||||
.editContainer {
|
||||
display: inline-block;
|
||||
margin-left: 4px;
|
||||
padding-left: 2px;
|
||||
border-left: 1px solid #eee;
|
||||
}
|
||||
|
||||
.editButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import styles from './TagInputTag.css';
|
||||
|
||||
@@ -25,59 +24,24 @@ class TagInputTag extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
onEdit = () => {
|
||||
const {
|
||||
index,
|
||||
tag,
|
||||
onEdit
|
||||
} = this.props;
|
||||
|
||||
onEdit({
|
||||
index,
|
||||
id: tag.id,
|
||||
value: tag.name
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
tag,
|
||||
kind,
|
||||
canEdit
|
||||
kind
|
||||
} = this.props;
|
||||
return (
|
||||
<div
|
||||
<Link
|
||||
className={styles.tag}
|
||||
tabIndex={-1}
|
||||
onPress={this.onDelete}
|
||||
>
|
||||
<Label
|
||||
kind={kind}
|
||||
>
|
||||
<Link
|
||||
tabIndex={-1}
|
||||
onPress={this.onDelete}
|
||||
>
|
||||
|
||||
{tag.name}
|
||||
</Link>
|
||||
|
||||
{
|
||||
canEdit ?
|
||||
<div className={styles.editContainer}>
|
||||
<IconButton
|
||||
className={styles.editButton}
|
||||
name={icons.EDIT}
|
||||
size={9}
|
||||
onPress={this.onEdit}
|
||||
/>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
<Label kind={kind}>
|
||||
{tag.name}
|
||||
</Label>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -86,9 +50,7 @@ TagInputTag.propTypes = {
|
||||
index: PropTypes.number.isRequired,
|
||||
tag: PropTypes.shape(tagShape),
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
canEdit: PropTypes.bool.isRequired,
|
||||
onDelete: PropTypes.func.isRequired,
|
||||
onEdit: PropTypes.func.isRequired
|
||||
onDelete: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default TagInputTag;
|
||||
|
||||
@@ -1,102 +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 TagInput from './TagInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { value }) => value,
|
||||
(state, { values }) => values,
|
||||
(tags, tagList) => {
|
||||
const sortedTags = _.sortBy(tagList, 'value');
|
||||
|
||||
return {
|
||||
tags: tags.reduce((acc, tag) => {
|
||||
const matchingTag = _.find(tagList, { key: tag });
|
||||
|
||||
if (matchingTag) {
|
||||
acc.push({
|
||||
id: tag,
|
||||
name: matchingTag.value
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []),
|
||||
|
||||
tagList: sortedTags.map(({ key: id, value: name }) => {
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
}),
|
||||
|
||||
allTags: sortedTags
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class TagSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTagAdd = (tag) => {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
allTags
|
||||
} = this.props;
|
||||
|
||||
const existingTag =_.some(allTags, { key: tag.id });
|
||||
|
||||
const newValue = value.slice();
|
||||
|
||||
if (existingTag) {
|
||||
newValue.push(tag.id);
|
||||
}
|
||||
|
||||
this.props.onChange({ name, value: newValue });
|
||||
}
|
||||
|
||||
onTagDelete = ({ index }) => {
|
||||
const {
|
||||
name,
|
||||
value
|
||||
} = this.props;
|
||||
|
||||
const newValue = value.slice();
|
||||
newValue.splice(index, 1);
|
||||
|
||||
this.props.onChange({
|
||||
name,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TagInput
|
||||
onTagAdd={this.onTagAdd}
|
||||
onTagDelete={this.onTagDelete}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TagSelectInputConnector.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
allTags: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(TagSelectInputConnector);
|
||||
@@ -130,8 +130,7 @@ class TextInput extends Component {
|
||||
step,
|
||||
min,
|
||||
max,
|
||||
onBlur,
|
||||
onCopy
|
||||
onBlur
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -156,8 +155,6 @@ class TextInput extends Component {
|
||||
onChange={this.onChange}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={onBlur}
|
||||
onCopy={onCopy}
|
||||
onCut={onCopy}
|
||||
onKeyUp={this.onKeyUp}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onMouseUp={this.onMouseUp}
|
||||
@@ -183,7 +180,6 @@ TextInput.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
onCopy: PropTypes.func,
|
||||
onSelectionChange: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
.inputWrapper {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.inputFolder {
|
||||
composes: input from '~Components/Form/Input.css';
|
||||
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
.inputUnitWrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.inputUnit {
|
||||
composes: inputUnit from '~Components/Form/FormInputGroup.css';
|
||||
|
||||
right: 40px;
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
||||
.unit {
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-top: 5px;
|
||||
margin-left: 17px;
|
||||
line-height: 20px;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
|
||||
label {
|
||||
flex: 0 0 50px;
|
||||
}
|
||||
|
||||
.value {
|
||||
width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.unit {
|
||||
width: 90px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.readOnly {
|
||||
background-color: #eee;
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
/* eslint-disable no-bitwise */
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import styles from './UMaskInput.css';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
const umaskOptions = [
|
||||
{
|
||||
key: '755',
|
||||
value: '755 - Owner write, Everyone else read',
|
||||
hint: 'drwxr-xr-x'
|
||||
},
|
||||
{
|
||||
key: '775',
|
||||
value: '775 - Owner & Group write, Other read',
|
||||
hint: 'drwxrwxr-x'
|
||||
},
|
||||
{
|
||||
key: '770',
|
||||
value: '770 - Owner & Group write',
|
||||
hint: 'drwxrwx---'
|
||||
},
|
||||
{
|
||||
key: '750',
|
||||
value: '750 - Owner write, Group read',
|
||||
hint: 'drwxr-x---'
|
||||
},
|
||||
{
|
||||
key: '777',
|
||||
value: '777 - Everyone write',
|
||||
hint: 'drwxrwxrwx'
|
||||
}
|
||||
];
|
||||
|
||||
function formatPermissions(permissions) {
|
||||
|
||||
const hasSticky = permissions & 0o1000;
|
||||
const hasSetGID = permissions & 0o2000;
|
||||
const hasSetUID = permissions & 0o4000;
|
||||
|
||||
let result = '';
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const bit = (permissions & (1 << i)) !== 0;
|
||||
let digit = bit ? 'xwr'[i % 3] : '-';
|
||||
if (i === 6 && hasSetUID) {
|
||||
digit = bit ? 's' : 'S';
|
||||
} else if (i === 3 && hasSetGID) {
|
||||
digit = bit ? 's' : 'S';
|
||||
} else if (i === 0 && hasSticky) {
|
||||
digit = bit ? 't' : 'T';
|
||||
}
|
||||
result = digit + result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
class UMaskInput extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const valueNum = parseInt(value, 8);
|
||||
const umaskNum = 0o777 & ~valueNum;
|
||||
const umask = umaskNum.toString(8).padStart(4, '0');
|
||||
const folderNum = 0o777 & ~umaskNum;
|
||||
const folder = folderNum.toString(8).padStart(3, '0');
|
||||
const fileNum = 0o666 & ~umaskNum;
|
||||
const file = fileNum.toString(8).padStart(3, '0');
|
||||
|
||||
const unit = formatPermissions(folderNum);
|
||||
|
||||
const values = umaskOptions.map((v) => {
|
||||
return { ...v, hint: <span className={styles.unit}>{v.hint}</span> };
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.inputWrapper}>
|
||||
<div className={styles.inputUnitWrapper}>
|
||||
<EnhancedSelectInput
|
||||
name={name}
|
||||
value={value}
|
||||
values={values}
|
||||
isEditable={true}
|
||||
onChange={onChange}
|
||||
/>
|
||||
|
||||
<div className={styles.inputUnit}>
|
||||
d{unit}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.details}>
|
||||
<div>
|
||||
<label>UMask</label>
|
||||
<div className={styles.value}>{umask}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>Folder</label>
|
||||
<div className={styles.value}>{folder}</div>
|
||||
<div className={styles.unit}>d{formatPermissions(folderNum)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<label>File</label>
|
||||
<div className={styles.value}>{file}</div>
|
||||
<div className={styles.unit}>{formatPermissions(fileNum)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UMaskInput.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func,
|
||||
onBlur: PropTypes.func
|
||||
};
|
||||
|
||||
export default UMaskInput;
|
||||
@@ -47,6 +47,10 @@ class Link extends Component {
|
||||
el = 'a';
|
||||
linkProps.href = to;
|
||||
linkProps.target = target || '_self';
|
||||
} else if (to.startsWith(`${window.Sonarr.urlBase}/`)) {
|
||||
el = RouterLink;
|
||||
linkProps.to = to;
|
||||
linkProps.target = target;
|
||||
} else {
|
||||
el = RouterLink;
|
||||
linkProps.to = `${window.Sonarr.urlBase}/${to.replace(/^\//, '')}`;
|
||||
|
||||
@@ -53,7 +53,7 @@ class PageHeader extends Component {
|
||||
<div className={styles.logoContainer}>
|
||||
<Link
|
||||
className={styles.logoLink}
|
||||
to={'/'}
|
||||
to={`${window.Sonarr.urlBase}/`}
|
||||
>
|
||||
<img
|
||||
className={styles.logo}
|
||||
|
||||
@@ -29,7 +29,7 @@ const links = [
|
||||
to: '/add/new'
|
||||
},
|
||||
{
|
||||
title: 'Library Import',
|
||||
title: 'Import',
|
||||
to: '/add/import'
|
||||
},
|
||||
{
|
||||
|
||||
@@ -22,8 +22,6 @@ function getMaxWidth() {
|
||||
} else {
|
||||
maxWidth = 450;
|
||||
}
|
||||
|
||||
return maxWidth;
|
||||
}
|
||||
|
||||
class Tooltip extends Component {
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -1,120 +0,0 @@
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
|
||||
// This file contains some helpers for power users in a browser console
|
||||
|
||||
let hasWarned = false;
|
||||
|
||||
function checkActivationWarning() {
|
||||
if (!hasWarned) {
|
||||
console.log('Activated SonarrApi console helpers.');
|
||||
console.warn('Be warned: There will be no further confirmation checks.');
|
||||
hasWarned = true;
|
||||
}
|
||||
}
|
||||
|
||||
function attachAsyncActions(promise) {
|
||||
promise.filter = function() {
|
||||
const args = arguments;
|
||||
const res = this.then((d) => d.filter(...args));
|
||||
attachAsyncActions(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
promise.map = function() {
|
||||
const args = arguments;
|
||||
const res = this.then((d) => d.map(...args));
|
||||
attachAsyncActions(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
promise.all = function() {
|
||||
const res = this.then((d) => Promise.all(d));
|
||||
attachAsyncActions(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
promise.forEach = function(action) {
|
||||
const res = this.then((d) => Promise.all(d.map(action)));
|
||||
attachAsyncActions(res);
|
||||
return res;
|
||||
};
|
||||
}
|
||||
|
||||
class ResourceApi {
|
||||
constructor(api, url) {
|
||||
this.api = api;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
single(id) {
|
||||
return this.api.fetch(`${this.url}/${id}`);
|
||||
}
|
||||
|
||||
all() {
|
||||
return this.api.fetch(this.url);
|
||||
}
|
||||
|
||||
filter(pred) {
|
||||
return this.all().filter(pred);
|
||||
}
|
||||
|
||||
update(resource) {
|
||||
return this.api.fetch(`${this.url}/${resource.id}`, { method: 'PUT', data: resource });
|
||||
}
|
||||
|
||||
delete(resource) {
|
||||
if (typeof resource === 'object' && resource !== null && resource.id) {
|
||||
resource = resource.id;
|
||||
}
|
||||
|
||||
if (!resource || !Number.isInteger(resource)) {
|
||||
throw Error('Invalid resource', resource);
|
||||
}
|
||||
|
||||
return this.api.fetch(`${this.url}/${resource}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
fetch(url, options) {
|
||||
return this.api.fetch(`${this.url}${url}`, options);
|
||||
}
|
||||
}
|
||||
|
||||
class ConsoleApi {
|
||||
constructor() {
|
||||
this.series = new ResourceApi(this, '/series');
|
||||
}
|
||||
|
||||
resource(url) {
|
||||
return new ResourceApi(this, url);
|
||||
}
|
||||
|
||||
fetch(url, options) {
|
||||
checkActivationWarning();
|
||||
|
||||
options = options || {};
|
||||
|
||||
const req = {
|
||||
url,
|
||||
method: options.method || 'GET'
|
||||
};
|
||||
|
||||
if (options.data) {
|
||||
req.dataType = 'json';
|
||||
req.data = JSON.stringify(options.data);
|
||||
}
|
||||
|
||||
const promise = createAjaxRequest(req).request;
|
||||
|
||||
promise.fail((xhr) => {
|
||||
console.error(`Failed to fetch ${url}`, xhr);
|
||||
});
|
||||
|
||||
attachAsyncActions(promise);
|
||||
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
window.SonarrApi = new ConsoleApi();
|
||||
|
||||
export default ConsoleApi;
|
||||
@@ -1,13 +1,22 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment } from 'react';
|
||||
import padNumber from 'Utilities/Number/padNumber';
|
||||
import filterAlternateTitles from 'Utilities/Series/filterAlternateTitles';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import SceneInfo from './SceneInfo';
|
||||
import styles from './EpisodeNumber.css';
|
||||
|
||||
function getAlternateTitles(seasonNumber, sceneSeasonNumber, alternateTitles) {
|
||||
return alternateTitles.filter((alternateTitle) => {
|
||||
if (sceneSeasonNumber && sceneSeasonNumber === alternateTitle.sceneSeasonNumber) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return seasonNumber === alternateTitle.seasonNumber;
|
||||
});
|
||||
}
|
||||
|
||||
function getWarningMessage(unverifiedSceneNumbering, seriesType, absoluteEpisodeNumber) {
|
||||
const messages = [];
|
||||
|
||||
@@ -30,14 +39,13 @@ function EpisodeNumber(props) {
|
||||
sceneSeasonNumber,
|
||||
sceneEpisodeNumber,
|
||||
sceneAbsoluteEpisodeNumber,
|
||||
useSceneNumbering,
|
||||
unverifiedSceneNumbering,
|
||||
alternateTitles: seriesAlternateTitles,
|
||||
seriesType,
|
||||
showSeasonNumber
|
||||
} = props;
|
||||
|
||||
const alternateTitles = filterAlternateTitles(seriesAlternateTitles, null, useSceneNumbering, seasonNumber, sceneSeasonNumber);
|
||||
const alternateTitles = getAlternateTitles(seasonNumber, sceneSeasonNumber, seriesAlternateTitles);
|
||||
|
||||
const hasSceneInformation = sceneSeasonNumber !== undefined ||
|
||||
sceneEpisodeNumber !== undefined ||
|
||||
@@ -73,8 +81,6 @@ function EpisodeNumber(props) {
|
||||
title="Scene Information"
|
||||
body={
|
||||
<SceneInfo
|
||||
seasonNumber={seasonNumber}
|
||||
episodeNumber={episodeNumber}
|
||||
sceneSeasonNumber={sceneSeasonNumber}
|
||||
sceneEpisodeNumber={sceneEpisodeNumber}
|
||||
sceneAbsoluteEpisodeNumber={sceneAbsoluteEpisodeNumber}
|
||||
@@ -125,7 +131,6 @@ EpisodeNumber.propTypes = {
|
||||
sceneSeasonNumber: PropTypes.number,
|
||||
sceneEpisodeNumber: PropTypes.number,
|
||||
sceneAbsoluteEpisodeNumber: PropTypes.number,
|
||||
useSceneNumbering: PropTypes.bool.isRequired,
|
||||
unverifiedSceneNumbering: PropTypes.bool.isRequired,
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
seriesType: PropTypes.string,
|
||||
@@ -133,7 +138,6 @@ EpisodeNumber.propTypes = {
|
||||
};
|
||||
|
||||
EpisodeNumber.defaultProps = {
|
||||
useSceneNumbering: false,
|
||||
unverifiedSceneNumbering: false,
|
||||
alternateTitles: [],
|
||||
showSeasonNumber: false
|
||||
|
||||
@@ -15,8 +15,3 @@
|
||||
|
||||
margin-left: 100px;
|
||||
}
|
||||
|
||||
.comment {
|
||||
color: $darkGray;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import styles from './SceneInfo.css';
|
||||
|
||||
function SceneInfo(props) {
|
||||
const {
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
sceneSeasonNumber,
|
||||
sceneEpisodeNumber,
|
||||
sceneAbsoluteEpisodeNumber,
|
||||
@@ -58,33 +56,14 @@ function SceneInfo(props) {
|
||||
<div>
|
||||
{
|
||||
alternateTitles.map((alternateTitle) => {
|
||||
let suffix = '';
|
||||
|
||||
const altSceneSeasonNumber = sceneSeasonNumber === undefined ? seasonNumber : sceneSeasonNumber;
|
||||
const altSceneEpisodeNumber = sceneEpisodeNumber === undefined ? episodeNumber : sceneEpisodeNumber;
|
||||
|
||||
const mappingSeasonNumber = alternateTitle.sceneOrigin === 'tvdb' ? seasonNumber : altSceneSeasonNumber;
|
||||
const altSeasonNumber = (alternateTitle.sceneSeasonNumber !== -1 && alternateTitle.sceneSeasonNumber !== undefined) ? alternateTitle.sceneSeasonNumber : mappingSeasonNumber;
|
||||
const altEpisodeNumber = alternateTitle.sceneOrigin === 'tvdb' ? episodeNumber : altSceneEpisodeNumber;
|
||||
|
||||
if (altEpisodeNumber !== altSceneEpisodeNumber) {
|
||||
suffix = `S${padNumber(altSeasonNumber, 2)}E${padNumber(altEpisodeNumber, 2)}`;
|
||||
} else if (altSeasonNumber !== altSceneSeasonNumber) {
|
||||
suffix = `S${padNumber(altSeasonNumber, 2)}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={alternateTitle.title}
|
||||
>
|
||||
{alternateTitle.title}
|
||||
{
|
||||
suffix &&
|
||||
<span> ({suffix})</span>
|
||||
}
|
||||
{
|
||||
alternateTitle.comment &&
|
||||
<span className={styles.comment}> {alternateTitle.comment}</span>
|
||||
alternateTitle.sceneSeasonNumber !== -1 &&
|
||||
<span> (S{padNumber(alternateTitle.sceneSeasonNumber, 2)})</span>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
@@ -99,8 +78,6 @@ function SceneInfo(props) {
|
||||
}
|
||||
|
||||
SceneInfo.propTypes = {
|
||||
seasonNumber: PropTypes.number,
|
||||
episodeNumber: PropTypes.number,
|
||||
sceneSeasonNumber: PropTypes.number,
|
||||
sceneEpisodeNumber: PropTypes.number,
|
||||
sceneAbsoluteEpisodeNumber: PropTypes.number,
|
||||
|
||||
@@ -179,7 +179,6 @@ export const RESTORE = fasHistory;
|
||||
export const REORDER = fasBars;
|
||||
export const RSS = fasRss;
|
||||
export const SAVE = fasSave;
|
||||
export const SCENE_MAPPING = fasSitemap;
|
||||
export const SCHEDULED = farClock;
|
||||
export const SCORE = fasUserPlus;
|
||||
export const SEARCH = fasSearch;
|
||||
|
||||
@@ -18,8 +18,6 @@ export const SERIES_TYPE_SELECT = 'seriesTypeSelect';
|
||||
export const TAG = 'tag';
|
||||
export const TEXT = 'text';
|
||||
export const TEXT_TAG = 'textTag';
|
||||
export const TAG_SELECT = 'tagSelect';
|
||||
export const UMASK = 'umask';
|
||||
|
||||
export const all = [
|
||||
AUTO_COMPLETE,
|
||||
@@ -41,7 +39,5 @@ export const all = [
|
||||
SERIES_TYPE_SELECT,
|
||||
TAG,
|
||||
TEXT,
|
||||
TEXT_TAG,
|
||||
TAG_SELECT,
|
||||
UMASK
|
||||
TEXT_TAG
|
||||
];
|
||||
|
||||
@@ -93,7 +93,6 @@ class SelectEpisodeModalContent extends Component {
|
||||
error,
|
||||
items,
|
||||
relativePath,
|
||||
isAnime,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
onSortPress,
|
||||
@@ -173,10 +172,8 @@ class SelectEpisodeModalContent extends Component {
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
episodeNumber={item.episodeNumber}
|
||||
absoluteEpisodeNumber={item.absoluteEpisodeNumber}
|
||||
title={item.title}
|
||||
airDate={item.airDate}
|
||||
isAnime={isAnime}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
@@ -232,7 +229,6 @@ SelectEpisodeModalContent.propTypes = {
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
relativePath: PropTypes.string,
|
||||
isAnime: PropTypes.bool.isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.string,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
updateInteractiveImportItem,
|
||||
fetchInteractiveImportEpisodes,
|
||||
setInteractiveImportEpisodesSort,
|
||||
clearInteractiveImportEpisodes,
|
||||
reprocessInteractiveImportItems
|
||||
clearInteractiveImportEpisodes
|
||||
} from 'Store/Actions/interactiveImportActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import SelectEpisodeModalContent from './SelectEpisodeModalContent';
|
||||
@@ -22,11 +21,10 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchInteractiveImportEpisodes: fetchInteractiveImportEpisodes,
|
||||
dispatchSetInteractiveImportEpisodesSort: setInteractiveImportEpisodesSort,
|
||||
dispatchClearInteractiveImportEpisodes: clearInteractiveImportEpisodes,
|
||||
dispatchUpdateInteractiveImportItem: updateInteractiveImportItem,
|
||||
dispatchReprocessInteractiveImportItems: reprocessInteractiveImportItems
|
||||
fetchInteractiveImportEpisodes,
|
||||
setInteractiveImportEpisodesSort,
|
||||
clearInteractiveImportEpisodes,
|
||||
updateInteractiveImportItem
|
||||
};
|
||||
|
||||
class SelectEpisodeModalContentConnector extends Component {
|
||||
@@ -40,28 +38,26 @@ class SelectEpisodeModalContentConnector extends Component {
|
||||
seasonNumber
|
||||
} = this.props;
|
||||
|
||||
this.props.dispatchFetchInteractiveImportEpisodes({ seriesId, seasonNumber });
|
||||
this.props.fetchInteractiveImportEpisodes({ seriesId, seasonNumber });
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
// This clears the episodes for the queue and hides the queue
|
||||
// We'll need another place to store episodes for manual import
|
||||
this.props.dispatchClearInteractiveImportEpisodes();
|
||||
this.props.clearInteractiveImportEpisodes();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSortPress = (sortKey, sortDirection) => {
|
||||
this.props.dispatchSetInteractiveImportEpisodesSort({ sortKey, sortDirection });
|
||||
this.props.setInteractiveImportEpisodesSort({ sortKey, sortDirection });
|
||||
}
|
||||
|
||||
onEpisodesSelect = (episodeIds) => {
|
||||
const {
|
||||
ids,
|
||||
items,
|
||||
dispatchUpdateInteractiveImportItem,
|
||||
dispatchReprocessInteractiveImportItems,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
@@ -82,14 +78,12 @@ class SelectEpisodeModalContentConnector extends Component {
|
||||
const startingIndex = index * episodesPerFile;
|
||||
const episodes = sortedEpisodes.slice(startingIndex, startingIndex + episodesPerFile);
|
||||
|
||||
dispatchUpdateInteractiveImportItem({
|
||||
this.props.updateInteractiveImportItem({
|
||||
id,
|
||||
episodes
|
||||
});
|
||||
});
|
||||
|
||||
dispatchReprocessInteractiveImportItems({ ids });
|
||||
|
||||
onModalClose(true);
|
||||
}
|
||||
|
||||
@@ -112,11 +106,10 @@ SelectEpisodeModalContentConnector.propTypes = {
|
||||
seriesId: PropTypes.number.isRequired,
|
||||
seasonNumber: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
dispatchFetchInteractiveImportEpisodes: PropTypes.func.isRequired,
|
||||
dispatchSetInteractiveImportEpisodesSort: PropTypes.func.isRequired,
|
||||
dispatchClearInteractiveImportEpisodes: PropTypes.func.isRequired,
|
||||
dispatchUpdateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired,
|
||||
fetchInteractiveImportEpisodes: PropTypes.func.isRequired,
|
||||
setInteractiveImportEpisodesSort: PropTypes.func.isRequired,
|
||||
clearInteractiveImportEpisodes: PropTypes.func.isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -25,10 +25,8 @@ class SelectEpisodeRow extends Component {
|
||||
const {
|
||||
id,
|
||||
episodeNumber,
|
||||
absoluteEpisodeNumber,
|
||||
title,
|
||||
airDate,
|
||||
isAnime,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
@@ -43,7 +41,6 @@ class SelectEpisodeRow extends Component {
|
||||
|
||||
<TableRowCell>
|
||||
{episodeNumber}
|
||||
{isAnime ? ` (${absoluteEpisodeNumber})` : ''}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
@@ -61,10 +58,8 @@ class SelectEpisodeRow extends Component {
|
||||
SelectEpisodeRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
episodeNumber: PropTypes.number.isRequired,
|
||||
absoluteEpisodeNumber: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
airDate: PropTypes.string.isRequired,
|
||||
isAnime: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -124,7 +124,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
|
||||
name={icons.QUICK}
|
||||
/>
|
||||
|
||||
Move Automatically
|
||||
Quick Import
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -187,17 +187,8 @@ class InteractiveImportRow extends Component {
|
||||
} = this.state;
|
||||
|
||||
const seriesTitle = series ? series.title : '';
|
||||
const isAnime = series ? series.seriesType === 'anime' : false;
|
||||
|
||||
const episodeInfo = episodes.map((episode) => {
|
||||
return (
|
||||
<div key={episode.id}>
|
||||
{episode.episodeNumber}
|
||||
{isAnime ? ` (${episode.absoluteEpisodeNumber})` : ''}
|
||||
{` - ${episode.title}`}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
const episodeNumbers = episodes.map((episode) => episode.episodeNumber)
|
||||
.join(', ');
|
||||
|
||||
const showSeriesPlaceholder = isSelected && !series;
|
||||
const showSeasonNumberPlaceholder = isSelected && !!series && isNaN(seasonNumber) && !isReprocessing;
|
||||
@@ -255,7 +246,7 @@ class InteractiveImportRow extends Component {
|
||||
onPress={this.onSelectEpisodePress}
|
||||
>
|
||||
{
|
||||
showEpisodeNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : episodeInfo
|
||||
showEpisodeNumbersPlaceholder ? <InteractiveImportRowCellPlaceholder /> : episodeNumbers
|
||||
}
|
||||
</TableRowCellButton>
|
||||
|
||||
@@ -348,7 +339,6 @@ class InteractiveImportRow extends Component {
|
||||
isOpen={isSelectEpisodeModalOpen}
|
||||
ids={[id]}
|
||||
seriesId={series && series.id}
|
||||
isAnime={isAnime}
|
||||
seasonNumber={seasonNumber}
|
||||
relativePath={relativePath}
|
||||
onModalClose={this.onSelectEpisodeModalClose}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import InteractiveImportSelectFolderModalContentConnector from './Folder/InteractiveImportSelectFolderModalContentConnector';
|
||||
import InteractiveImportModalContentConnector from './Interactive/InteractiveImportModalContentConnector';
|
||||
@@ -48,7 +47,6 @@ class InteractiveImportModal extends Component {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchLanguageProfileSchema } from 'Store/Actions/settingsActions';
|
||||
import { updateInteractiveImportItems, reprocessInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
|
||||
import { updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
|
||||
import SelectLanguageModalContent from './SelectLanguageModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -30,8 +30,7 @@ function createMapStateToProps() {
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchLanguageProfileSchema: fetchLanguageProfileSchema,
|
||||
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
|
||||
dispatchReprocessInteractiveImportItems: reprocessInteractiveImportItems
|
||||
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems
|
||||
};
|
||||
|
||||
class SelectLanguageModalContentConnector extends Component {
|
||||
@@ -49,23 +48,15 @@ class SelectLanguageModalContentConnector extends Component {
|
||||
// Listeners
|
||||
|
||||
onLanguageSelect = ({ value }) => {
|
||||
const {
|
||||
ids,
|
||||
dispatchUpdateInteractiveImportItems,
|
||||
dispatchReprocessInteractiveImportItems
|
||||
} = this.props;
|
||||
|
||||
const languageId = parseInt(value);
|
||||
const language = _.find(this.props.items,
|
||||
(item) => item.language.id === languageId).language;
|
||||
|
||||
dispatchUpdateInteractiveImportItems({
|
||||
ids,
|
||||
this.props.dispatchUpdateInteractiveImportItems({
|
||||
ids: this.props.ids,
|
||||
language
|
||||
});
|
||||
|
||||
dispatchReprocessInteractiveImportItems({ ids });
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
@@ -90,7 +81,6 @@ SelectLanguageModalContentConnector.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
dispatchFetchLanguageProfileSchema: PropTypes.func.isRequired,
|
||||
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
|
||||
dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import getQualities from 'Utilities/Quality/getQualities';
|
||||
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||
import { updateInteractiveImportItems, reprocessInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
|
||||
import { updateInteractiveImportItems } from 'Store/Actions/interactiveImportActions';
|
||||
import SelectQualityModalContent from './SelectQualityModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -31,8 +31,7 @@ function createMapStateToProps() {
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchQualityProfileSchema: fetchQualityProfileSchema,
|
||||
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems,
|
||||
dispatchReprocessInteractiveImportItems: reprocessInteractiveImportItems
|
||||
dispatchUpdateInteractiveImportItems: updateInteractiveImportItems
|
||||
};
|
||||
|
||||
class SelectQualityModalContentConnector extends Component {
|
||||
@@ -50,12 +49,6 @@ class SelectQualityModalContentConnector extends Component {
|
||||
// Listeners
|
||||
|
||||
onQualitySelect = ({ qualityId, proper, real }) => {
|
||||
const {
|
||||
ids,
|
||||
dispatchUpdateInteractiveImportItems,
|
||||
dispatchReprocessInteractiveImportItems
|
||||
} = this.props;
|
||||
|
||||
const quality = _.find(this.props.items,
|
||||
(item) => item.id === qualityId);
|
||||
|
||||
@@ -64,16 +57,14 @@ class SelectQualityModalContentConnector extends Component {
|
||||
real: real ? 1 : 0
|
||||
};
|
||||
|
||||
dispatchUpdateInteractiveImportItems({
|
||||
ids,
|
||||
this.props.dispatchUpdateInteractiveImportItems({
|
||||
ids: this.props.ids,
|
||||
quality: {
|
||||
quality,
|
||||
revision
|
||||
}
|
||||
});
|
||||
|
||||
dispatchReprocessInteractiveImportItems({ ids });
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
@@ -98,7 +89,6 @@ SelectQualityModalContentConnector.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
|
||||
dispatchUpdateInteractiveImportItems: PropTypes.func.isRequired,
|
||||
dispatchReprocessInteractiveImportItems: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ class SelectSeriesModalContent extends Component {
|
||||
// Listeners
|
||||
|
||||
onFilterChange = ({ value }) => {
|
||||
this.setState({ filter: value });
|
||||
this.setState({ filter: value.toLowerCase() });
|
||||
}
|
||||
|
||||
//
|
||||
@@ -42,7 +42,6 @@ class SelectSeriesModalContent extends Component {
|
||||
} = this.props;
|
||||
|
||||
const filter = this.state.filter;
|
||||
const filterLower = filter.toLowerCase();
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
@@ -69,7 +68,7 @@ class SelectSeriesModalContent extends Component {
|
||||
>
|
||||
{
|
||||
items.map((item) => {
|
||||
return item.title.toLowerCase().includes(filterLower) ?
|
||||
return item.title.toLowerCase().includes(filter) ?
|
||||
(
|
||||
<SelectSeriesRow
|
||||
key={item.id}
|
||||
|
||||
@@ -7,16 +7,9 @@
|
||||
.title {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.sceneMapping {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.indexer {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeLanguage from 'Episode/EpisodeLanguage';
|
||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import ReleaseSceneIndicator from './ReleaseSceneIndicator';
|
||||
import Peers from './Peers';
|
||||
import styles from './InteractiveSearchRow.css';
|
||||
|
||||
@@ -115,17 +114,8 @@ class InteractiveSearchRow extends Component {
|
||||
quality,
|
||||
language,
|
||||
preferredWordScore,
|
||||
sceneMapping,
|
||||
seasonNumber,
|
||||
episodeNumbers,
|
||||
absoluteEpisodeNumbers,
|
||||
mappedSeasonNumber,
|
||||
mappedEpisodeNumbers,
|
||||
mappedAbsoluteEpisodeNumbers,
|
||||
rejections,
|
||||
episodeRequested,
|
||||
downloadAllowed,
|
||||
isDaily,
|
||||
isGrabbing,
|
||||
isGrabbed,
|
||||
longDateFormat,
|
||||
@@ -152,18 +142,6 @@ class InteractiveSearchRow extends Component {
|
||||
<Link to={infoUrl}>
|
||||
{title}
|
||||
</Link>
|
||||
<ReleaseSceneIndicator
|
||||
className={styles.sceneMapping}
|
||||
seasonNumber={mappedSeasonNumber}
|
||||
episodeNumbers={mappedEpisodeNumbers}
|
||||
absoluteEpisodeNumbers={mappedAbsoluteEpisodeNumbers}
|
||||
sceneSeasonNumber={seasonNumber}
|
||||
sceneEpisodeNumbers={episodeNumbers}
|
||||
sceneAbsoluteEpisodeNumbers={absoluteEpisodeNumbers}
|
||||
sceneMapping={sceneMapping}
|
||||
episodeRequested={episodeRequested}
|
||||
isDaily={isDaily}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.indexer}>
|
||||
@@ -267,17 +245,8 @@ InteractiveSearchRow.propTypes = {
|
||||
quality: PropTypes.object.isRequired,
|
||||
language: PropTypes.object.isRequired,
|
||||
preferredWordScore: PropTypes.number.isRequired,
|
||||
sceneMapping: PropTypes.object,
|
||||
seasonNumber: PropTypes.number,
|
||||
episodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
||||
absoluteEpisodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
||||
mappedSeasonNumber: PropTypes.number,
|
||||
mappedEpisodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
||||
mappedAbsoluteEpisodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
||||
rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
episodeRequested: PropTypes.bool.isRequired,
|
||||
downloadAllowed: PropTypes.bool.isRequired,
|
||||
isDaily: PropTypes.bool.isRequired,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isGrabbed: PropTypes.bool.isRequired,
|
||||
grabError: PropTypes.string,
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
.container {
|
||||
margin: 2px;
|
||||
padding: 0 2px;
|
||||
border: 1px solid;
|
||||
border-radius: 2px;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.messages {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.descriptionList {
|
||||
composes: descriptionList from '~Components/DescriptionList/DescriptionList.css';
|
||||
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
composes: title from '~Components/DescriptionList/DescriptionListItemTitle.css';
|
||||
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.description {
|
||||
composes: title from '~Components/DescriptionList/DescriptionListItemDescription.css';
|
||||
|
||||
margin-left: 100px;
|
||||
}
|
||||
|
||||
.levelMixed {
|
||||
border-color: $dangerColor;
|
||||
color: $dangerColor;
|
||||
}
|
||||
|
||||
.levelUnknown {
|
||||
border-color: $warningColor;
|
||||
color: $warningColor;
|
||||
}
|
||||
|
||||
.levelMapped {
|
||||
border-color: $textColor;
|
||||
color: $textColor;
|
||||
}
|
||||
|
||||
.levelNormal {
|
||||
border-color: $textColor;
|
||||
color: $textColor;
|
||||
}
|
||||
|
||||
.levelNone {
|
||||
border-color: $textColor;
|
||||
color: $textColor;
|
||||
opacity: 0.2;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.levelNotRequested {
|
||||
border-color: $dangerColor;
|
||||
color: $dangerColor;
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { tooltipPositions, icons, sizes } from 'Helpers/Props';
|
||||
import styles from './ReleaseSceneIndicator.css';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Icon from 'Components/Icon';
|
||||
|
||||
function formatReleaseNumber(seasonNumber, episodeNumbers, absoluteEpisodeNumbers) {
|
||||
if (episodeNumbers && episodeNumbers.length) {
|
||||
if (episodeNumbers.length > 1) {
|
||||
return `${seasonNumber}x${episodeNumbers[0]}-${episodeNumbers[episodeNumbers.length - 1]}`;
|
||||
}
|
||||
return `${seasonNumber}x${episodeNumbers[0]}`;
|
||||
}
|
||||
|
||||
if (absoluteEpisodeNumbers && absoluteEpisodeNumbers.length) {
|
||||
if (absoluteEpisodeNumbers.length > 1) {
|
||||
return `${absoluteEpisodeNumbers[0]}-${absoluteEpisodeNumbers[absoluteEpisodeNumbers.length - 1]}`;
|
||||
}
|
||||
return absoluteEpisodeNumbers[0];
|
||||
}
|
||||
|
||||
if (seasonNumber !== undefined) {
|
||||
return `Season ${seasonNumber}`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function ReleaseSceneIndicator(props) {
|
||||
const {
|
||||
className,
|
||||
seasonNumber,
|
||||
episodeNumbers,
|
||||
absoluteEpisodeNumbers,
|
||||
sceneSeasonNumber,
|
||||
sceneEpisodeNumbers,
|
||||
sceneAbsoluteEpisodeNumbers,
|
||||
sceneMapping,
|
||||
episodeRequested,
|
||||
isDaily
|
||||
} = props;
|
||||
|
||||
const {
|
||||
sceneOrigin,
|
||||
title,
|
||||
comment
|
||||
} = sceneMapping || {};
|
||||
|
||||
if (isDaily) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let mappingDifferent = (sceneSeasonNumber !== undefined && seasonNumber !== sceneSeasonNumber);
|
||||
|
||||
if (sceneEpisodeNumbers !== undefined) {
|
||||
mappingDifferent = mappingDifferent || !_.isEqual(sceneEpisodeNumbers, episodeNumbers);
|
||||
} else if (sceneAbsoluteEpisodeNumbers !== undefined) {
|
||||
mappingDifferent = mappingDifferent || !_.isEqual(sceneAbsoluteEpisodeNumbers, absoluteEpisodeNumbers);
|
||||
}
|
||||
|
||||
if (!sceneMapping && !mappingDifferent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const releaseNumber = formatReleaseNumber(sceneSeasonNumber, sceneEpisodeNumbers, sceneAbsoluteEpisodeNumbers);
|
||||
const mappedNumber = formatReleaseNumber(seasonNumber, episodeNumbers, absoluteEpisodeNumbers);
|
||||
const messages = [];
|
||||
|
||||
const isMixed = (sceneOrigin === 'mixed');
|
||||
const isUnknown = (sceneOrigin === 'unknown' || sceneOrigin === 'unknown:tvdb');
|
||||
|
||||
let level = styles.levelNone;
|
||||
|
||||
if (isMixed) {
|
||||
level = styles.levelMixed;
|
||||
messages.push(<div>{comment ?? 'Source'} releases exist with ambiguous numbering, unable to reliably identify episode.</div>);
|
||||
} else if (isUnknown) {
|
||||
level = styles.levelUnknown;
|
||||
messages.push(<div>Numbering varies for this episode and release does not match any known mappings.</div>);
|
||||
if (sceneOrigin === 'unknown') {
|
||||
messages.push(<div>Assuming Scene numbering.</div>);
|
||||
} else if (sceneOrigin === 'unknown:tvdb') {
|
||||
messages.push(<div>Assuming TheTVDB numbering.</div>);
|
||||
}
|
||||
} else if (mappingDifferent) {
|
||||
level = styles.levelMapped;
|
||||
} else if (sceneOrigin) {
|
||||
level = styles.levelNormal;
|
||||
}
|
||||
|
||||
if (!episodeRequested) {
|
||||
if (!isMixed && !isUnknown) {
|
||||
level = styles.levelNotRequested;
|
||||
}
|
||||
if (mappedNumber) {
|
||||
messages.push(<div>Mapped episode wasn't requested in this search.</div>);
|
||||
} else {
|
||||
messages.push(<div>Unknown episode or series.</div>);
|
||||
}
|
||||
}
|
||||
|
||||
const table = (
|
||||
<DescriptionList className={styles.descriptionList}>
|
||||
{
|
||||
comment !== undefined &&
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
title="Mapping"
|
||||
data={comment}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
title !== undefined &&
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
title="Title"
|
||||
data={title}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
releaseNumber !== undefined &&
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
title="Release"
|
||||
data={releaseNumber ?? 'unknown'}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
releaseNumber !== undefined &&
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
title="TheTVDB"
|
||||
data={mappedNumber ?? 'unknown'}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
anchor={
|
||||
<div className={classNames(level, styles.container, className)}>
|
||||
<Icon
|
||||
name={icons.SCENE_MAPPING}
|
||||
size={sizes.SMALL}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
title="Scene Info"
|
||||
body={
|
||||
<div>
|
||||
{table}
|
||||
{
|
||||
messages.length &&
|
||||
<div className={styles.messages}>
|
||||
{messages}
|
||||
</div> || null
|
||||
}
|
||||
</div>
|
||||
}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ReleaseSceneIndicator.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
seasonNumber: PropTypes.number,
|
||||
episodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
||||
absoluteEpisodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
||||
sceneSeasonNumber: PropTypes.number,
|
||||
sceneEpisodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
||||
sceneAbsoluteEpisodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
||||
sceneMapping: PropTypes.object.isRequired,
|
||||
episodeRequested: PropTypes.bool.isRequired,
|
||||
isDaily: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default ReleaseSceneIndicator;
|
||||
@@ -11,7 +11,7 @@ function createMapStateToProps() {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'series'
|
||||
customFilterType: 'seasonPass'
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -18,10 +18,10 @@ class SeasonPassRow extends Component {
|
||||
render() {
|
||||
const {
|
||||
seriesId,
|
||||
monitored,
|
||||
status,
|
||||
title,
|
||||
titleSlug,
|
||||
title,
|
||||
monitored,
|
||||
seasons,
|
||||
isSaving,
|
||||
isSelected,
|
||||
@@ -84,10 +84,10 @@ class SeasonPassRow extends Component {
|
||||
|
||||
SeasonPassRow.propTypes = {
|
||||
seriesId: PropTypes.number.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
seasons: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
|
||||
@@ -60,7 +60,6 @@ class EpisodeRow extends Component {
|
||||
sceneAbsoluteEpisodeNumber,
|
||||
airDateUtc,
|
||||
title,
|
||||
useSceneNumbering,
|
||||
unverifiedSceneNumbering,
|
||||
isSaving,
|
||||
seriesMonitored,
|
||||
@@ -111,7 +110,6 @@ class EpisodeRow extends Component {
|
||||
seasonNumber={seasonNumber}
|
||||
episodeNumber={episodeNumber}
|
||||
absoluteEpisodeNumber={absoluteEpisodeNumber}
|
||||
useSceneNumbering={useSceneNumbering}
|
||||
unverifiedSceneNumbering={unverifiedSceneNumbering}
|
||||
seriesType={seriesType}
|
||||
sceneSeasonNumber={sceneSeasonNumber}
|
||||
@@ -267,7 +265,6 @@ EpisodeRow.propTypes = {
|
||||
airDateUtc: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
isSaving: PropTypes.bool,
|
||||
useSceneNumbering: PropTypes.bool,
|
||||
unverifiedSceneNumbering: PropTypes.bool,
|
||||
seriesMonitored: PropTypes.bool.isRequired,
|
||||
seriesType: PropTypes.string.isRequired,
|
||||
|
||||
@@ -11,7 +11,6 @@ function createMapStateToProps() {
|
||||
createEpisodeFileSelector(),
|
||||
(series = {}, episodeFile) => {
|
||||
return {
|
||||
useSceneNumbering: series.useSceneNumbering,
|
||||
seriesMonitored: series.monitored,
|
||||
seriesType: series.seriesType,
|
||||
episodeFilePath: episodeFile ? episodeFile.path : null,
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
.alternateTitle {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.comment {
|
||||
color: $darkGray;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
|
||||
@@ -9,14 +9,10 @@ function SeriesAlternateTitles({ alternateTitles }) {
|
||||
alternateTitles.map((alternateTitle) => {
|
||||
return (
|
||||
<li
|
||||
key={alternateTitle.title}
|
||||
key={alternateTitle}
|
||||
className={styles.alternateTitle}
|
||||
>
|
||||
{alternateTitle.title}
|
||||
{
|
||||
alternateTitle.comment &&
|
||||
<span className={styles.comment}> {alternateTitle.comment}</span>
|
||||
}
|
||||
{alternateTitle}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -34,7 +34,6 @@ import SeriesAlternateTitles from './SeriesAlternateTitles';
|
||||
import SeriesDetailsSeasonConnector from './SeriesDetailsSeasonConnector';
|
||||
import SeriesTagsConnector from './SeriesTagsConnector';
|
||||
import SeriesDetailsLinks from './SeriesDetailsLinks';
|
||||
import MonitoringOptionsModal from 'Series/MonitoringOptions/MonitoringOptionsModal';
|
||||
import { getSeriesStatusDetails } from 'Series/SeriesStatus';
|
||||
import styles from './SeriesDetails.css';
|
||||
|
||||
@@ -72,7 +71,6 @@ class SeriesDetails extends Component {
|
||||
isDeleteSeriesModalOpen: false,
|
||||
isSeriesHistoryModalOpen: false,
|
||||
isInteractiveImportModalOpen: false,
|
||||
isMonitorOptionsModalOpen: false,
|
||||
allExpanded: false,
|
||||
allCollapsed: false,
|
||||
expandedState: {},
|
||||
@@ -134,14 +132,6 @@ class SeriesDetails extends Component {
|
||||
this.setState({ isSeriesHistoryModalOpen: false });
|
||||
}
|
||||
|
||||
onMonitorOptionsPress = () => {
|
||||
this.setState({ isMonitorOptionsModalOpen: true });
|
||||
}
|
||||
|
||||
onMonitorOptionsClose = () => {
|
||||
this.setState({ isMonitorOptionsModalOpen: false });
|
||||
}
|
||||
|
||||
onExpandAllPress = () => {
|
||||
const {
|
||||
allExpanded,
|
||||
@@ -221,7 +211,6 @@ class SeriesDetails extends Component {
|
||||
isDeleteSeriesModalOpen,
|
||||
isSeriesHistoryModalOpen,
|
||||
isInteractiveImportModalOpen,
|
||||
isMonitorOptionsModalOpen,
|
||||
allExpanded,
|
||||
allCollapsed,
|
||||
expandedState,
|
||||
@@ -299,12 +288,6 @@ class SeriesDetails extends Component {
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="Series Monitoring"
|
||||
iconName={icons.MONITORED}
|
||||
onPress={this.onMonitorOptionsPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Edit"
|
||||
iconName={icons.EDIT}
|
||||
@@ -316,7 +299,6 @@ class SeriesDetails extends Component {
|
||||
iconName={icons.DELETE}
|
||||
onPress={this.onDeleteSeriesPress}
|
||||
/>
|
||||
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
@@ -444,7 +426,7 @@ class SeriesDetails extends Component {
|
||||
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
formatBytes(sizeOnDisk || 0)
|
||||
formatBytes(sizeOnDisk)
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
@@ -664,12 +646,6 @@ class SeriesDetails extends Component {
|
||||
showImportMode={false}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
|
||||
<MonitoringOptionsModal
|
||||
isOpen={isMonitorOptionsModalOpen}
|
||||
seriesId={id}
|
||||
onModalClose={this.onMonitorOptionsClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
@@ -688,7 +664,6 @@ SeriesDetails.propTypes = {
|
||||
statistics: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
monitor: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
network: PropTypes.string,
|
||||
overview: PropTypes.string.isRequired,
|
||||
@@ -697,7 +672,6 @@ SeriesDetails.propTypes = {
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isRefreshing: PropTypes.bool.isRequired,
|
||||
isSearching: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import filterAlternateTitles from 'Utilities/Series/filterAlternateTitles';
|
||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions';
|
||||
@@ -110,7 +109,14 @@ function createMapStateToProps() {
|
||||
|
||||
const isFetching = isEpisodesFetching || isEpisodeFilesFetching;
|
||||
const isPopulated = isEpisodesPopulated && isEpisodeFilesPopulated;
|
||||
const alternateTitles = filterAlternateTitles(series.alternateTitles, series.title, series.useSceneNumbering);
|
||||
const alternateTitles = _.reduce(series.alternateTitles, (acc, alternateTitle) => {
|
||||
if ((alternateTitle.seasonNumber === -1 || alternateTitle.seasonNumber === undefined) &&
|
||||
(alternateTitle.sceneSeasonNumber === -1 || alternateTitle.sceneSeasonNumber === undefined)) {
|
||||
acc.push(alternateTitle.title);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...series,
|
||||
|
||||
@@ -323,7 +323,7 @@ class SeriesDetailsSeason extends Component {
|
||||
|
||||
<MenuContent className={styles.actionsMenuContent}>
|
||||
<MenuItem
|
||||
isDisabled={isSearching || !hasMonitoredEpisodes || !seriesMonitored}
|
||||
isDisabled={isSearching || !hasMonitoredEpisodes}
|
||||
onPress={onSearchPress}
|
||||
>
|
||||
<SpinnerIcon
|
||||
@@ -389,10 +389,10 @@ class SeriesDetailsSeason extends Component {
|
||||
<SpinnerIconButton
|
||||
className={styles.actionButton}
|
||||
name={icons.SEARCH}
|
||||
title={hasMonitoredEpisodes && seriesMonitored ? 'Search for monitored episodes in this season' : 'No monitored episodes in this season'}
|
||||
title={hasMonitoredEpisodes ? 'Search for monitored episodes in this season' : 'No monitored episodes in this season'}
|
||||
size={24}
|
||||
isSpinning={isSearching}
|
||||
isDisabled={isSearching || !hasMonitoredEpisodes || !seriesMonitored}
|
||||
isDisabled={isSearching || !hasMonitoredEpisodes}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ function createMapStateToProps() {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'series'
|
||||
customFilterType: 'seriesEditor'
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -27,17 +27,17 @@ class SeriesEditorRow extends Component {
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
monitored,
|
||||
status,
|
||||
title,
|
||||
titleSlug,
|
||||
seriesType,
|
||||
qualityProfile,
|
||||
title,
|
||||
monitored,
|
||||
languageProfile,
|
||||
path,
|
||||
tags,
|
||||
qualityProfile,
|
||||
seriesType,
|
||||
seasonFolder,
|
||||
path,
|
||||
statistics = {},
|
||||
tags,
|
||||
columns,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
|
||||
@@ -144,15 +144,6 @@ function SeriesIndexSortMenu(props) {
|
||||
>
|
||||
Size on Disk
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="tags"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Tags
|
||||
</SortMenuItem>
|
||||
</MenuContent>
|
||||
</SortMenu>
|
||||
);
|
||||
|
||||
@@ -97,8 +97,7 @@ class SeriesIndexPoster extends Component {
|
||||
seasonCount,
|
||||
episodeCount,
|
||||
episodeFileCount,
|
||||
totalEpisodeCount,
|
||||
sizeOnDisk
|
||||
totalEpisodeCount
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
@@ -227,7 +226,6 @@ class SeriesIndexPoster extends Component {
|
||||
|
||||
<SeriesIndexPosterInfo
|
||||
seasonCount={seasonCount}
|
||||
sizeOnDisk={sizeOnDisk}
|
||||
qualityProfile={qualityProfile}
|
||||
showQualityProfile={showQualityProfile}
|
||||
showRelativeDates={showRelativeDates}
|
||||
|
||||
@@ -11,7 +11,7 @@ function createMapStateToProps() {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'series'
|
||||
customFilterType: 'seriesIndex'
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import MonitoringOptionsModal from './EditSeriesModal';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearPendingChanges
|
||||
};
|
||||
|
||||
class MonitoringOptionsModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'series' });
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MonitoringOptionsModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MonitoringOptionsModalConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
clearPendingChanges: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(MonitoringOptionsModalConnector);
|
||||
@@ -1,25 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import MonitoringOptionsModalContentConnector from './MonitoringOptionsModalContentConnector';
|
||||
|
||||
function MonitoringOptionsModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<MonitoringOptionsModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
MonitoringOptionsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MonitoringOptionsModal;
|
||||
@@ -1,132 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
const NO_CHANGE = 'noChange';
|
||||
|
||||
class MonitoringOptionsModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
monitor: NO_CHANGE
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isSaving,
|
||||
saveError
|
||||
} = prevProps;
|
||||
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
this.setState({
|
||||
monitor: NO_CHANGE
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.setState({ [name]: value });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSavePress = () => {
|
||||
const {
|
||||
onSavePress
|
||||
} = this.props;
|
||||
const {
|
||||
monitor
|
||||
} = this.state;
|
||||
|
||||
if (monitor !== NO_CHANGE) {
|
||||
onSavePress({ monitor });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isSaving,
|
||||
onInputChange,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitor
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Monitor Series
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>Monitoring</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MONITOR_EPISODES_SELECT}
|
||||
name="monitor"
|
||||
value={monitor}
|
||||
includeNoChange={true}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<SpinnerButton
|
||||
isSpinning={isSaving}
|
||||
onPress={this.onSavePress}
|
||||
>
|
||||
Save
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MonitoringOptionsModalContent.propTypes = {
|
||||
seriesId: PropTypes.number.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
MonitoringOptionsModalContent.defaultProps = {
|
||||
isSaving: false
|
||||
};
|
||||
|
||||
export default MonitoringOptionsModalContent;
|
||||
@@ -1,77 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { updateSeriesMonitor } from 'Store/Actions/seriesActions';
|
||||
import MonitoringOptionsModalContent from './MonitoringOptionsModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.series,
|
||||
(seriesState) => {
|
||||
const {
|
||||
isSaving,
|
||||
saveError
|
||||
} = seriesState;
|
||||
|
||||
return {
|
||||
isSaving,
|
||||
saveError
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchUpdateMonitoringOptions: updateSeriesMonitor
|
||||
};
|
||||
|
||||
class MonitoringOptionsModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.setState({ name, value });
|
||||
}
|
||||
|
||||
onSavePress = ({ monitor }) => {
|
||||
this.props.dispatchUpdateMonitoringOptions({
|
||||
id: this.props.seriesId,
|
||||
monitor
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<MonitoringOptionsModalContent
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MonitoringOptionsModalContentConnector.propTypes = {
|
||||
seriesId: PropTypes.number.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
dispatchUpdateMonitoringOptions: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MonitoringOptionsModalContentConnector);
|
||||
@@ -59,7 +59,7 @@ function UpdateSettings(props) {
|
||||
type={inputTypes.AUTO_COMPLETE}
|
||||
name="branch"
|
||||
helpText={usingExternalUpdateMechanism ? 'Branch used by external update mechanism' : 'Branch to use to update Sonarr'}
|
||||
helpLink="https://wiki.servarr.com/Sonarr_Settings#Updates"
|
||||
helpLink="https://github.com/Sonarr/Sonarr/wiki/Release-Branches"
|
||||
{...branch}
|
||||
values={branchValues}
|
||||
onChange={onInputChange}
|
||||
@@ -97,7 +97,7 @@ function UpdateSettings(props) {
|
||||
name="updateMechanism"
|
||||
values={updateOptions}
|
||||
helpText="Use Sonarr's built-in updater or a script"
|
||||
helpLink="https://wiki.servarr.com/Sonarr_Settings#Updates"
|
||||
helpLink="https://github.com/Sonarr/Sonarr/wiki/Updating"
|
||||
onChange={onInputChange}
|
||||
{...updateMechanism}
|
||||
/>
|
||||
|
||||
@@ -89,7 +89,7 @@ function IndexerOptions(props) {
|
||||
unit="minutes"
|
||||
helpText="Interval in minutes. Set to zero to disable (this will stop all automatic release grabbing)"
|
||||
helpTextWarning="This will apply to all indexers, please follow the rules set forth by them"
|
||||
helpLink="https://wiki.servarr.com/Sonarr_FAQ#How_does_Sonarr_find_episodes"
|
||||
helpLink="https://github.com/Sonarr/Sonarr/wiki/RSS-Sync"
|
||||
onChange={onInputChange}
|
||||
{...settings.rssSyncInterval}
|
||||
/>
|
||||
|
||||
@@ -238,7 +238,7 @@ class MediaManagement extends Component {
|
||||
legend="File Management"
|
||||
>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>Unmonitor Deleted Episodes</FormLabel>
|
||||
<FormLabel>Ignore Deleted Episodes</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
@@ -382,32 +382,17 @@ class MediaManagement extends Component {
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>chmod Folder</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.UMASK}
|
||||
name="chmodFolder"
|
||||
helpText="Octal, applied during import/rename to media folders and files (without execute bits)"
|
||||
helpTextWarning="This only works if the user running sonarr is the owner of the file. It's better to ensure the download client sets the permissions properly."
|
||||
onChange={onInputChange}
|
||||
{...settings.chmodFolder}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>chown Group</FormLabel>
|
||||
<FormLabel>File chmod mode</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="chownGroup"
|
||||
helpText="Group name or gid. Use gid for remote file systems."
|
||||
helpTextWarning="This only works if the user running sonarr is the owner of the file. It's better to ensure the download client uses the same group as sonarr."
|
||||
values={fileDateOptions}
|
||||
name="fileChmod"
|
||||
helpTexts={[
|
||||
'Octal, applied to media files when imported/renamed by Sonarr',
|
||||
'The same mode is applied to series/season folders with the execute bit added, e.g., 0644 becomes 0755'
|
||||
]}
|
||||
onChange={onInputChange}
|
||||
{...settings.chownGroup}
|
||||
{...settings.fileChmod}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
|
||||
@@ -42,17 +42,11 @@ function EditNotificationModalContent(props) {
|
||||
onDownload,
|
||||
onUpgrade,
|
||||
onRename,
|
||||
onSeriesDelete,
|
||||
onEpisodeFileDelete,
|
||||
onEpisodeFileDeleteForUpgrade,
|
||||
onHealthIssue,
|
||||
supportsOnGrab,
|
||||
supportsOnDownload,
|
||||
supportsOnUpgrade,
|
||||
supportsOnRename,
|
||||
supportsOnSeriesDelete,
|
||||
supportsOnEpisodeFileDelete,
|
||||
supportsOnEpisodeFileDeleteForUpgrade,
|
||||
supportsOnHealthIssue,
|
||||
includeHealthWarnings,
|
||||
tags,
|
||||
@@ -156,49 +150,6 @@ function EditNotificationModalContent(props) {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>On Series Delete</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="onSeriesDelete"
|
||||
helpText="Be notified when series are deleted"
|
||||
isDisabled={!supportsOnSeriesDelete.value}
|
||||
{...onSeriesDelete}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>On Episode File Delete</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="onEpisodeFileDelete"
|
||||
helpText="Be notified when episode files are deleted"
|
||||
isDisabled={!supportsOnEpisodeFileDelete.value}
|
||||
{...onEpisodeFileDelete}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
onEpisodeFileDelete.value ?
|
||||
<FormGroup>
|
||||
<FormLabel>On Episode File Delete For Upgrade</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="onEpisodeFileDeleteForUpgrade"
|
||||
helpText="Be notified when episode files are deleted for upgrades"
|
||||
isDisabled={!supportsOnEpisodeFileDeleteForUpgrade.value}
|
||||
{...onEpisodeFileDeleteForUpgrade}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>On Health Issue</FormLabel>
|
||||
|
||||
|
||||
@@ -58,17 +58,11 @@ class Notification extends Component {
|
||||
onDownload,
|
||||
onUpgrade,
|
||||
onRename,
|
||||
onSeriesDelete,
|
||||
onEpisodeFileDelete,
|
||||
onEpisodeFileDeleteForUpgrade,
|
||||
onHealthIssue,
|
||||
supportsOnGrab,
|
||||
supportsOnDownload,
|
||||
supportsOnUpgrade,
|
||||
supportsOnRename,
|
||||
supportsOnSeriesDelete,
|
||||
supportsOnEpisodeFileDelete,
|
||||
supportsOnEpisodeFileDeleteForUpgrade,
|
||||
supportsOnHealthIssue
|
||||
} = this.props;
|
||||
|
||||
@@ -83,78 +77,48 @@ class Notification extends Component {
|
||||
</div>
|
||||
|
||||
{
|
||||
supportsOnGrab && onGrab ?
|
||||
supportsOnGrab && onGrab &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Grab
|
||||
</Label> :
|
||||
null
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnDownload && onDownload ?
|
||||
supportsOnDownload && onDownload &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Import
|
||||
</Label> :
|
||||
null
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnUpgrade && onDownload && onUpgrade ?
|
||||
supportsOnUpgrade && onDownload && onUpgrade &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Upgrade
|
||||
</Label> :
|
||||
null
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnRename && onRename ?
|
||||
supportsOnRename && onRename &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Rename
|
||||
</Label> :
|
||||
null
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnHealthIssue && onHealthIssue ?
|
||||
supportsOnHealthIssue && onHealthIssue &&
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Health Issue
|
||||
</Label> :
|
||||
null
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnSeriesDelete && onSeriesDelete ?
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Series Delete
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnEpisodeFileDelete && onEpisodeFileDelete ?
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Episode File Delete
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
supportsOnEpisodeFileDeleteForUpgrade && onEpisodeFileDelete && onEpisodeFileDeleteForUpgrade ?
|
||||
<Label kind={kinds.SUCCESS}>
|
||||
On Episode File Delete For Upgrade
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!onGrab && !onDownload && !onRename && !onHealthIssue && !onSeriesDelete && !onEpisodeFileDelete ?
|
||||
!onGrab && !onDownload && !onRename && !onHealthIssue &&
|
||||
<Label
|
||||
kind={kinds.DISABLED}
|
||||
outline={true}
|
||||
>
|
||||
Disabled
|
||||
</Label> :
|
||||
null
|
||||
</Label>
|
||||
}
|
||||
|
||||
<EditNotificationModalConnector
|
||||
@@ -185,15 +149,9 @@ Notification.propTypes = {
|
||||
onDownload: PropTypes.bool.isRequired,
|
||||
onUpgrade: PropTypes.bool.isRequired,
|
||||
onRename: PropTypes.bool.isRequired,
|
||||
onSeriesDelete: PropTypes.bool.isRequired,
|
||||
onEpisodeFileDelete: PropTypes.bool.isRequired,
|
||||
onEpisodeFileDeleteForUpgrade: PropTypes.bool.isRequired,
|
||||
onHealthIssue: PropTypes.bool.isRequired,
|
||||
supportsOnGrab: PropTypes.bool.isRequired,
|
||||
supportsOnDownload: PropTypes.bool.isRequired,
|
||||
supportsOnSeriesDelete: PropTypes.bool.isRequired,
|
||||
supportsOnEpisodeFileDelete: PropTypes.bool.isRequired,
|
||||
supportsOnEpisodeFileDeleteForUpgrade: PropTypes.bool.isRequired,
|
||||
supportsOnUpgrade: PropTypes.bool.isRequired,
|
||||
supportsOnRename: PropTypes.bool.isRequired,
|
||||
supportsOnHealthIssue: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
.horizontalScroll {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.delayProfiles {
|
||||
user-select: none;
|
||||
}
|
||||
@@ -29,10 +25,3 @@
|
||||
width: $dragHandleWidth;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.horizontalScroll {
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, scrollDirections } from 'Helpers/Props';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Measure from 'Components/Measure';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import DelayProfileDragSource from './DelayProfileDragSource';
|
||||
import DelayProfileDragPreview from './DelayProfileDragPreview';
|
||||
import DelayProfile from './DelayProfile';
|
||||
@@ -72,59 +71,48 @@ class DelayProfiles extends Component {
|
||||
errorMessage="Unable to load Delay Profiles"
|
||||
{...otherProps}
|
||||
>
|
||||
<Scroller
|
||||
className={styles.horizontalScroll}
|
||||
scrollDirection={
|
||||
scrollDirections.HORIZONTAL
|
||||
<div className={styles.delayProfilesHeader}>
|
||||
<div className={styles.column}>Protocol</div>
|
||||
<div className={styles.column}>Usenet Delay</div>
|
||||
<div className={styles.column}>Torrent Delay</div>
|
||||
<div className={styles.tags}>Tags</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.delayProfiles}>
|
||||
{
|
||||
items.map((item, index) => {
|
||||
return (
|
||||
<DelayProfileDragSource
|
||||
key={item.id}
|
||||
tagList={tagList}
|
||||
{...item}
|
||||
{...otherProps}
|
||||
index={index}
|
||||
isDragging={isDragging}
|
||||
isDraggingUp={isDraggingUp}
|
||||
isDraggingDown={isDraggingDown}
|
||||
onConfirmDeleteDelayProfile={onConfirmDeleteDelayProfile}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
autoFocus={false}
|
||||
>
|
||||
<div>
|
||||
<div className={styles.delayProfilesHeader}>
|
||||
<div className={styles.column}>Protocol</div>
|
||||
<div className={styles.column}>Usenet Delay</div>
|
||||
<div className={styles.column}>Torrent Delay</div>
|
||||
<div className={styles.tags}>Tags</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.delayProfiles}>
|
||||
{
|
||||
items.map((item, index) => {
|
||||
return (
|
||||
<DelayProfileDragSource
|
||||
key={item.id}
|
||||
tagList={tagList}
|
||||
{...item}
|
||||
{...otherProps}
|
||||
index={index}
|
||||
isDragging={isDragging}
|
||||
isDraggingUp={isDraggingUp}
|
||||
isDraggingDown={isDraggingDown}
|
||||
onConfirmDeleteDelayProfile={onConfirmDeleteDelayProfile}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
<DelayProfileDragPreview
|
||||
width={width}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DelayProfileDragPreview
|
||||
width={width}
|
||||
{
|
||||
defaultProfile &&
|
||||
<div>
|
||||
<DelayProfile
|
||||
tagList={tagList}
|
||||
isDragging={false}
|
||||
onConfirmDeleteDelayProfile={onConfirmDeleteDelayProfile}
|
||||
{...defaultProfile}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
defaultProfile ?
|
||||
<div>
|
||||
<DelayProfile
|
||||
tagList={tagList}
|
||||
isDragging={false}
|
||||
onConfirmDeleteDelayProfile={onConfirmDeleteDelayProfile}
|
||||
{...defaultProfile}
|
||||
/>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</Scroller>
|
||||
}
|
||||
|
||||
<div className={styles.addDelayProfile}>
|
||||
<Link
|
||||
|
||||
@@ -62,14 +62,13 @@ function EditReleaseProfileModalContent(props) {
|
||||
<FormLabel>Must Contain</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
{...required}
|
||||
type={inputTypes.TEXT_TAG}
|
||||
name="required"
|
||||
helpText="The release must contain at least one of these terms (case insensitive)"
|
||||
kind={kinds.SUCCESS}
|
||||
placeholder="Add new restriction"
|
||||
delimiters={tagInputDelimiters}
|
||||
canEdit={true}
|
||||
{...required}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -78,14 +77,13 @@ function EditReleaseProfileModalContent(props) {
|
||||
<FormLabel>Must Not Contain</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
{...ignored}
|
||||
type={inputTypes.TEXT_TAG}
|
||||
name="ignored"
|
||||
helpText="The release will be rejected if it contains one or more of terms (case insensitive)"
|
||||
kind={kinds.DANGER}
|
||||
placeholder="Add new restriction"
|
||||
delimiters={tagInputDelimiters}
|
||||
canEdit={true}
|
||||
{...ignored}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -128,7 +126,6 @@ function EditReleaseProfileModalContent(props) {
|
||||
type={inputTypes.INDEXER_SELECT}
|
||||
name="indexerId"
|
||||
helpText="Specify what indexer the profile applies to"
|
||||
helpTextWarning="Using a specific indexer with preferred words can lead to duplicate releases being grabbed"
|
||||
{...indexerId}
|
||||
includeAny={true}
|
||||
onChange={onInputChange}
|
||||
|
||||
@@ -13,7 +13,6 @@ import styles from './QualityDefinition.css';
|
||||
|
||||
const MIN = 0;
|
||||
const MAX = 400;
|
||||
const MIN_DISTANCE = 1;
|
||||
|
||||
const slider = {
|
||||
min: MIN,
|
||||
@@ -188,7 +187,7 @@ class QualityDefinition extends Component {
|
||||
min={slider.min}
|
||||
max={slider.max}
|
||||
step={slider.step}
|
||||
minDistance={MIN_DISTANCE * 5}
|
||||
minDistance={10}
|
||||
value={[sliderMinSize, sliderMaxSize]}
|
||||
withTracks={true}
|
||||
snapDragDisabled={true}
|
||||
@@ -244,7 +243,7 @@ class QualityDefinition extends Component {
|
||||
name={`${id}.min`}
|
||||
value={minSize || MIN}
|
||||
min={MIN}
|
||||
max={maxSize ? maxSize - MIN_DISTANCE : MAX - MIN_DISTANCE}
|
||||
max={maxSize ? maxSize - 10 : MAX - 10}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
onChange={this.onMinSizeChange}
|
||||
@@ -256,9 +255,9 @@ class QualityDefinition extends Component {
|
||||
|
||||
<NumberInput
|
||||
className={styles.sizeInput}
|
||||
name={`${id}.max`}
|
||||
name={`${id}.min`}
|
||||
value={maxSize || MAX}
|
||||
min={minSize + MIN_DISTANCE}
|
||||
min={minSize + 10}
|
||||
max={MAX}
|
||||
step={0.1}
|
||||
isFloat={true}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getProviderState from 'Utilities/State/getProviderState';
|
||||
import { set, updateItem } from '../baseActions';
|
||||
|
||||
const abortCurrentRequests = {};
|
||||
let lastSaveData = null;
|
||||
|
||||
export function createCancelSaveProviderHandler(section) {
|
||||
return function(getState, payload, dispatch) {
|
||||
@@ -28,33 +26,25 @@ function createSaveProviderHandler(section, url, options = {}) {
|
||||
} = payload;
|
||||
|
||||
const saveData = getProviderState({ id, ...otherPayload }, getState, section);
|
||||
const requestUrl = id ? `${url}/${id}` : url;
|
||||
const params = { ...queryParams };
|
||||
|
||||
// If the user is re-saving the same provider without changes
|
||||
// force it to be saved. Only applies to editing existing providers.
|
||||
|
||||
if (id && _.isEqual(saveData, lastSaveData)) {
|
||||
params.forceSave = true;
|
||||
}
|
||||
|
||||
lastSaveData = saveData;
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${requestUrl}?${$.param(params, true)}`,
|
||||
method: id ? 'PUT' : 'POST',
|
||||
url: `${url}?${$.param(queryParams, true)}`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify(saveData)
|
||||
};
|
||||
|
||||
if (id) {
|
||||
ajaxOptions.url = `${url}/${id}?${$.param(queryParams, true)}`;
|
||||
ajaxOptions.method = 'PUT';
|
||||
}
|
||||
|
||||
const { request, abortRequest } = createAjaxRequest(ajaxOptions);
|
||||
|
||||
abortCurrentRequests[section] = abortRequest;
|
||||
|
||||
request.done((data) => {
|
||||
lastSaveData = null;
|
||||
|
||||
dispatch(batchActions([
|
||||
updateItem({ section, ...data }),
|
||||
|
||||
|
||||
@@ -106,9 +106,6 @@ export default {
|
||||
selectedSchema.onDownload = selectedSchema.supportsOnDownload;
|
||||
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
|
||||
selectedSchema.onRename = selectedSchema.supportsOnRename;
|
||||
selectedSchema.OnSeriesDelete = selectedSchema.supportsOnSeriesDelete;
|
||||
selectedSchema.OnEpisodeFileDelete = selectedSchema.supportsOnEpisodeFileDelete;
|
||||
selectedSchema.OnEpisodeFileDeleteForUpgrade = selectedSchema.supportsOnEpisodeFileDeleteForUpgrade;
|
||||
|
||||
return selectedSchema;
|
||||
});
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
@@ -9,7 +7,6 @@ import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptio
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||
import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers';
|
||||
import { set, updateItem } from './baseActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
@@ -27,7 +24,6 @@ export const defaultState = {
|
||||
sortDirection: sortDirections.DESCENDING,
|
||||
error: null,
|
||||
items: [],
|
||||
isRemoving: false,
|
||||
|
||||
columns: [
|
||||
{
|
||||
@@ -91,8 +87,7 @@ export const GOTO_LAST_BLACKLIST_PAGE = 'blacklist/gotoBlacklistLastPage';
|
||||
export const GOTO_BLACKLIST_PAGE = 'blacklist/gotoBlacklistPage';
|
||||
export const SET_BLACKLIST_SORT = 'blacklist/setBlacklistSort';
|
||||
export const SET_BLACKLIST_TABLE_OPTION = 'blacklist/setBlacklistTableOption';
|
||||
export const REMOVE_BLACKLIST_ITEM = 'blacklist/removeBlacklistItem';
|
||||
export const REMOVE_BLACKLIST_ITEMS = 'blacklist/removeBlacklistItems';
|
||||
export const REMOVE_FROM_BLACKLIST = 'blacklist/removeFromBlacklist';
|
||||
export const CLEAR_BLACKLIST = 'blacklist/clearBlacklist';
|
||||
|
||||
//
|
||||
@@ -106,8 +101,7 @@ export const gotoBlacklistLastPage = createThunk(GOTO_LAST_BLACKLIST_PAGE);
|
||||
export const gotoBlacklistPage = createThunk(GOTO_BLACKLIST_PAGE);
|
||||
export const setBlacklistSort = createThunk(SET_BLACKLIST_SORT);
|
||||
export const setBlacklistTableOption = createAction(SET_BLACKLIST_TABLE_OPTION);
|
||||
export const removeBlacklistItem = createThunk(REMOVE_BLACKLIST_ITEM);
|
||||
export const removeBlacklistItems = createThunk(REMOVE_BLACKLIST_ITEMS);
|
||||
export const removeFromBlacklist = createThunk(REMOVE_FROM_BLACKLIST);
|
||||
export const clearBlacklist = createAction(CLEAR_BLACKLIST);
|
||||
|
||||
//
|
||||
@@ -128,53 +122,7 @@ export const actionHandlers = handleThunks({
|
||||
[serverSideCollectionHandlers.SORT]: SET_BLACKLIST_SORT
|
||||
}),
|
||||
|
||||
[REMOVE_BLACKLIST_ITEM]: createRemoveItemHandler(section, '/blacklist'),
|
||||
|
||||
[REMOVE_BLACKLIST_ITEMS]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
ids
|
||||
} = payload;
|
||||
|
||||
dispatch(batchActions([
|
||||
...ids.map((id) => {
|
||||
return updateItem({
|
||||
section,
|
||||
id,
|
||||
isRemoving: true
|
||||
});
|
||||
}),
|
||||
|
||||
set({ section, isRemoving: true })
|
||||
]));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/blacklist/bulk',
|
||||
method: 'DELETE',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify({ ids })
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
// Don't use batchActions with thunks
|
||||
dispatch(fetchBlacklist());
|
||||
|
||||
dispatch(set({ section, isRemoving: false }));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(batchActions([
|
||||
...ids.map((id) => {
|
||||
return updateItem({
|
||||
section,
|
||||
id,
|
||||
isRemoving: false
|
||||
});
|
||||
}),
|
||||
|
||||
set({ section, isRemoving: false })
|
||||
]));
|
||||
});
|
||||
}
|
||||
[REMOVE_FROM_BLACKLIST]: createRemoveItemHandler(section, '/blacklist')
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import { isSameCommand } from 'Utilities/Command';
|
||||
@@ -7,7 +9,7 @@ import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||
import { showMessage, hideMessage } from './appActions';
|
||||
import { updateItem, removeItem } from './baseActions';
|
||||
import { updateItem } from './baseActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
@@ -35,9 +37,9 @@ export const defaultState = {
|
||||
export const FETCH_COMMANDS = 'commands/fetchCommands';
|
||||
export const EXECUTE_COMMAND = 'commands/executeCommand';
|
||||
export const CANCEL_COMMAND = 'commands/cancelCommand';
|
||||
export const ADD_COMMAND = 'commands/addCommand';
|
||||
export const UPDATE_COMMAND = 'commands/updateCommand';
|
||||
export const FINISH_COMMAND = 'commands/finishCommand';
|
||||
export const ADD_COMMAND = 'commands/updateCommand';
|
||||
export const UPDATE_COMMAND = 'commands/finishCommand';
|
||||
export const FINISH_COMMAND = 'commands/addCommand';
|
||||
export const REMOVE_COMMAND = 'commands/removeCommand';
|
||||
|
||||
//
|
||||
@@ -46,10 +48,10 @@ export const REMOVE_COMMAND = 'commands/removeCommand';
|
||||
export const fetchCommands = createThunk(FETCH_COMMANDS);
|
||||
export const executeCommand = createThunk(EXECUTE_COMMAND);
|
||||
export const cancelCommand = createThunk(CANCEL_COMMAND);
|
||||
export const addCommand = createThunk(ADD_COMMAND);
|
||||
export const updateCommand = createThunk(UPDATE_COMMAND);
|
||||
export const finishCommand = createThunk(FINISH_COMMAND);
|
||||
export const removeCommand = createThunk(REMOVE_COMMAND);
|
||||
export const addCommand = createAction(ADD_COMMAND);
|
||||
export const removeCommand = createAction(REMOVE_COMMAND);
|
||||
|
||||
//
|
||||
// Helpers
|
||||
@@ -159,10 +161,6 @@ export const actionHandlers = handleThunks({
|
||||
|
||||
[CANCEL_COMMAND]: createRemoveItemHandler(section, '/command'),
|
||||
|
||||
[ADD_COMMAND]: function(getState, payload, dispatch) {
|
||||
dispatch(updateItem({ section: 'commands', ...payload }));
|
||||
},
|
||||
|
||||
[UPDATE_COMMAND]: function(getState, payload, dispatch) {
|
||||
dispatch(updateItem({ section: 'commands', ...payload }));
|
||||
|
||||
@@ -185,10 +183,6 @@ export const actionHandlers = handleThunks({
|
||||
dispatch(updateItem({ section: 'commands', ...payload }));
|
||||
scheduleRemoveCommand(payload, dispatch);
|
||||
showCommandMessage(payload, dispatch);
|
||||
},
|
||||
|
||||
[ADD_COMMAND]: function(getState, payload, dispatch) {
|
||||
dispatch(removeItem({ section: 'commands', ...payload }));
|
||||
}
|
||||
|
||||
});
|
||||
@@ -196,4 +190,26 @@ export const actionHandlers = handleThunks({
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({}, defaultState, section);
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[ADD_COMMAND]: (state, { payload }) => {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.items = [...state.items, payload];
|
||||
|
||||
return newState;
|
||||
},
|
||||
|
||||
[REMOVE_COMMAND]: (state, { payload }) => {
|
||||
const newState = Object.assign({}, state);
|
||||
newState.items = [...state.items];
|
||||
|
||||
const index = _.findIndex(newState.items, { id: payload.id });
|
||||
|
||||
if (index > -1) {
|
||||
newState.items.splice(index, 1);
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user