1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-20 16:44:18 -04:00

Compare commits

..

1 Commits

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

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

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

View File

@@ -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.

View File

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

View File

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

View File

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

6
.github/SUPPORT.md vendored
View File

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

View File

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

1
.gitignore vendored
View File

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

View File

@@ -3,40 +3,25 @@
We're always looking for people to help make Sonarr even better, there are a number of ways to contribute.
## Documentation ##
Setup guides, [FAQ](https://wiki.servarr.com/Sonarr_FAQ), the more information we have on the [wiki](https://wiki.servarr.com/Sonarr) the better.
Setup guides, FAQ, the more information we have on the wiki the better.
## Development ##
### Tools required ###
- Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works (https://www.visualstudio.com/downloads/).
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
- [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download/) (Node 10.X.X or higher)
- [Yarn](https://yarnpkg.com/)
### Getting started ###
1. Fork Sonarr
2. Clone the repository into your development machine. [*info*](https://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)

View File

@@ -4,23 +4,17 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
## Getting Started
- [Download/Installation](https://sonarr.tv/#downloads-v3)
- [FAQ](https://wiki.servarr.com/Sonarr_FAQ)
- [Wiki](https://wiki.servarr.com/Sonarr)
- [(WIP) API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
- [Donate](https://sonarr.tv/donate)
- [Download](https://sonarr.tv/#download) (Linux, MacOS, Windows, Docker, etc.)
- [Installation](https://github.com/Sonarr/Sonarr/wiki/Installation)
- [FAQ](https://github.com/Sonarr/Sonarr/wiki/FAQ)
- [Wiki](https://github.com/Sonarr/Sonarr/wiki)
- [API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
## Support
Note: GitHub Issues are for Bugs and Feature Requests Only
- [Forums](https://forums.sonarr.tv/)
- [Donate](https://sonarr.tv/donate)
- [Discord](https://discord.gg/M6BvZn5)
- [GitHub - Bugs and Feature Requests Only](https://github.com/Sonarr/Sonarr/issues)
- [IRC ](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

View File

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

View File

@@ -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;

View File

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

View File

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

View File

@@ -10,8 +10,7 @@ gulp.task('build',
'webpack',
'copyHtml',
'copyFonts',
'copyImages',
'copyRobots'
'copyImages'
)
)
);

View File

@@ -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());
});

View File

@@ -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/**`
}

View File

@@ -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));

View File

@@ -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
};

View File

@@ -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,

View File

@@ -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
};

View File

@@ -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 }));
}
};
}

View File

@@ -31,8 +31,6 @@ class Queue extends Component {
constructor(props, context) {
super(props, context);
this._shouldBlockRefresh = false;
this.state = {
allSelected: false,
allUnselected: false,
@@ -44,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}
/>
);
})

View File

@@ -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 = {

View File

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

View File

@@ -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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;

View File

@@ -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,

View File

@@ -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}

View File

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

View File

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

View File

@@ -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
};

View File

@@ -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
};

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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
};

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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(/^\//, '')}`;

View File

@@ -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}

View File

@@ -29,7 +29,7 @@ const links = [
to: '/add/new'
},
{
title: 'Library Import',
title: 'Import',
to: '/add/import'
},
{

View File

@@ -22,8 +22,6 @@ function getMaxWidth() {
} else {
maxWidth = 450;
}
return maxWidth;
}
class Tooltip extends Component {

View File

@@ -1,2 +0,0 @@
User-agent: *
Disallow: /

View File

@@ -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;

View File

@@ -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

View File

@@ -15,8 +15,3 @@
margin-left: 100px;
}
.comment {
color: $darkGray;
font-size: $smallFontSize;
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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
];

View File

@@ -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,

View File

@@ -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
};

View File

@@ -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
};

View File

@@ -124,7 +124,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
name={icons.QUICK}
/>
Move Automatically
Quick Import
</Button>
</div>

View File

@@ -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}

View File

@@ -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}
>

View File

@@ -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
};

View File

@@ -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
};

View File

@@ -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}

View File

@@ -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';

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -11,7 +11,7 @@ function createMapStateToProps() {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'series'
customFilterType: 'seasonPass'
};
}
);

View File

@@ -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,

View File

@@ -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,

View File

@@ -11,7 +11,6 @@ function createMapStateToProps() {
createEpisodeFileSelector(),
(series = {}, episodeFile) => {
return {
useSceneNumbering: series.useSceneNumbering,
seriesMonitored: series.monitored,
seriesType: series.seriesType,
episodeFilePath: episodeFile ? episodeFile.path : null,

View File

@@ -1,8 +1,3 @@
.alternateTitle {
white-space: nowrap;
}
.comment {
color: $darkGray;
font-size: $smallFontSize;
}

View File

@@ -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>
);
})

View File

@@ -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,

View File

@@ -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,

View File

@@ -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}
/>

View File

@@ -11,7 +11,7 @@ function createMapStateToProps() {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'series'
customFilterType: 'seriesEditor'
};
}
);

View File

@@ -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

View File

@@ -144,15 +144,6 @@ function SeriesIndexSortMenu(props) {
>
Size on Disk
</SortMenuItem>
<SortMenuItem
name="tags"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
Tags
</SortMenuItem>
</MenuContent>
</SortMenu>
);

View File

@@ -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}

View File

@@ -11,7 +11,7 @@ function createMapStateToProps() {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'series'
customFilterType: 'seriesIndex'
};
}
);

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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}
/>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,

View File

@@ -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%;
}
}

View File

@@ -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

View File

@@ -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}

View File

@@ -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}

View File

@@ -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 }),

View File

@@ -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;
});

View File

@@ -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')
});
//

View File

@@ -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