1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-29 18:14: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
502 changed files with 3156 additions and 11416 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: ''
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

@@ -1,20 +1,14 @@
---
name: Feature request
about: Suggest an idea for Sonarr
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the problem**
A clear and concise description of the problem you're looking to solve.
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Describe any solutions you think might work**
A clear and concise description of any solutions or features you've considered.
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
Add any other context or screenshots about the feature request here.

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://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github)
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

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

View File

@@ -1 +1 @@
10
8

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -1,8 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import formatAge from 'Utilities/Number/formatAge';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import formatAge from 'Utilities/Number/formatAge';
import Link from 'Components/Link/Link';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
@@ -23,7 +22,6 @@ function HistoryDetails(props) {
const {
indexer,
releaseGroup,
preferredWordScore,
nzbInfoUrl,
downloadClient,
downloadId,
@@ -42,35 +40,24 @@ function HistoryDetails(props) {
/>
{
indexer ?
!!indexer &&
<DescriptionListItem
title="Indexer"
data={indexer}
/> :
null
/>
}
{
releaseGroup ?
!!releaseGroup &&
<DescriptionListItem
descriptionClassName={styles.description}
title="Release Group"
data={releaseGroup}
/> :
null
/>
}
{
preferredWordScore && preferredWordScore !== '0' ?
<DescriptionListItem
title="Preferred Word Score"
data={formatPreferredWordScore(preferredWordScore)}
/> :
null
}
{
nzbInfoUrl ?
!!nzbInfoUrl &&
<span>
<DescriptionListItemTitle>
Info URL
@@ -79,44 +66,39 @@ function HistoryDetails(props) {
<DescriptionListItemDescription>
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
</DescriptionListItemDescription>
</span> :
null
</span>
}
{
downloadClient ?
!!downloadClient &&
<DescriptionListItem
title="Download Client"
data={downloadClient}
/> :
null
/>
}
{
downloadId ?
!!downloadId &&
<DescriptionListItem
title="Grab ID"
data={downloadId}
/> :
null
/>
}
{
age || ageHours || ageMinutes ?
!!indexer &&
<DescriptionListItem
title="Age (when grabbed)"
data={formatAge(age, ageHours, ageMinutes)}
/> :
null
/>
}
{
publishedDate ?
!!publishedDate &&
<DescriptionListItem
title="Published Date"
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
/> :
null
/>
}
</DescriptionList>
);
@@ -136,12 +118,11 @@ function HistoryDetails(props) {
/>
{
message ?
!!message &&
<DescriptionListItem
title="Message"
data={message}
/> :
null
/>
}
</DescriptionList>
);
@@ -149,7 +130,6 @@ function HistoryDetails(props) {
if (eventType === 'downloadFolderImported') {
const {
preferredWordScore,
droppedPath,
importedPath
} = data;
@@ -163,32 +143,21 @@ function HistoryDetails(props) {
/>
{
droppedPath ?
!!droppedPath &&
<DescriptionListItem
descriptionClassName={styles.description}
title="Source"
data={droppedPath}
/> :
null
/>
}
{
importedPath ?
!!importedPath &&
<DescriptionListItem
descriptionClassName={styles.description}
title="Imported To"
data={importedPath}
/> :
null
}
{
preferredWordScore && preferredWordScore !== '0' ?
<DescriptionListItem
title="Preferred Word Score"
data={formatPreferredWordScore(preferredWordScore)}
/> :
null
/>
}
</DescriptionList>
);
@@ -196,8 +165,7 @@ function HistoryDetails(props) {
if (eventType === 'episodeFileDeleted') {
const {
reason,
preferredWordScore
reason
} = data;
let reasonMessage = '';
@@ -227,15 +195,6 @@ function HistoryDetails(props) {
title="Reason"
data={reasonMessage}
/>
{
preferredWordScore && preferredWordScore !== '0' ?
<DescriptionListItem
title="Preferred Word Score"
data={formatPreferredWordScore(preferredWordScore)}
/> :
null
}
</DescriptionList>
);
}
@@ -287,12 +246,11 @@ function HistoryDetails(props) {
/>
{
message ?
!!message &&
<DescriptionListItem
title="Message"
data={message}
/> :
null
/>
}
</DescriptionList>
);

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
@@ -85,21 +71,3 @@
display: inline-block;
margin: 5px -5px 5px 0;
}
.mobileCloseButtonContainer {
display: flex;
justify-content: flex-end;
height: 40px;
border-bottom: 1px solid $borderColor;
}
.mobileCloseButton {
width: 40px;
height: 40px;
text-align: center;
line-height: 40px;
&:hover {
color: $modalCloseButtonHoverColor;
}
}

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

@@ -1,5 +1,5 @@
.loadingMessage {
margin: 10px 10px 0;
margin: 50px 10px 0;
text-align: center;
font-weight: 300;
font-size: 36px;

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

@@ -1,12 +1,3 @@
.page {
composes: page from '~./Page.css';
}
.logoFull {
margin-top: 50px;
margin-right: auto;
margin-left: auto;
width: 48px;
height: 48px;
opacity: 0.65;
}

View File

@@ -3,15 +3,9 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import LoadingMessage from 'Components/Loading/LoadingMessage';
import styles from './LoadingPage.css';
const sonarrLogo = 'data:image/svg+xml;base64,PHN2ZyBoZWlnaHQ9IjIxNi45IiB2aWV3Qm94PSIwIDAgMjE2LjcgMjE2LjkiIHdpZHRoPSIyMTYuNyIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yMTYuNyAxMDguNDVjMCAyOS44MzMtMTAuNTMzIDU1LjQtMzEuNiA3Ni43LS43LjgzMy0xLjQ4MyAxLjYtMi4zNSAyLjMtMy40NjYgMy40LTcuMTMzIDYuNDg0LTExIDkuMjUtMTguMjY3IDEzLjQ2Ny0zOS4zNjcgMjAuMi02My4zIDIwLjItMjMuOTY3IDAtNDUuMDMzLTYuNzMzLTYzLjItMjAuMi00LjgtMy40LTkuMy03LjI1LTEzLjUtMTEuNTUtMTYuMzY3LTE2LjI2Ni0yNi40MTctMzUuMTY3LTMwLjE1LTU2LjctLjczMy00LjItMS4yMTctOC40NjctMS40NS0xMi44LS4xLTIuNC0uMTUtNC44LS4xNS03LjIgMC0yLjUzMy4wNS00Ljk1LjE1LTcuMjUgMC0uMjMzLjA2Ni0uNDY3LjItLjcgMS41NjctMjYuNiAxMi4wMzMtNDkuNTgzIDMxLjQtNjguOTVDNTMuMDUgMTAuNTE3IDc4LjYxNyAwIDEwOC40NSAwYzI5LjkzMyAwIDU1LjQ4NCAxMC41MTcgNzYuNjUgMzEuNTUgMjEuMDY3IDIxLjQzMyAzMS42IDQ3LjA2NyAzMS42IDc2Ljl6IiBmaWxsPSIjRUVFIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiLz4KICA8cGF0aCBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0xOTQuNjUgNDIuNWwtMjIuNCAyMi40QzE1OS4xNTIgNzcuOTk4IDE1OCA4OS40IDE1OCAxMDkuNWMwIDE3LjkzNCAyLjg1MiAzNC4zNTIgMTYuMiA0Ny43IDkuNzQ2IDkuNzQ2IDE5IDE4Ljk1IDE5IDE4Ljk1LTIuNSAzLjA2Ny01LjIgNi4wNjctOC4xIDktLjcuODMzLTEuNDgzIDEuNi0yLjM1IDIuMy0yLjUzMyAyLjUtNS4xNjcgNC44MTctNy45IDYuOTVsLTE3LjU1LTE3LjU1Yy0xNS41OTgtMTUuNi0yNy45OTYtMTcuMS00OC42LTE3LjEtMTkuNzcgMC0zMy4yMjMgMS44MjItNDcuNyAxNi4zLTguNjQ3IDguNjQ3LTE4LjU1IDE4LjYtMTguNTUgMTguNi0zLjc2Ny0yLjg2Ny03LjMzMy02LjAzNC0xMC43LTkuNS0yLjgtMi44LTUuNDE3LTUuNjY3LTcuODUtOC42IDAgMCA5Ljc5OC05Ljg0OCAxOS4xNS0xOS4yIDEzLjg1Mi0xMy44NTMgMTYuMS0yOS45MTYgMTYuMS00Ny44NSAwLTE3LjUtMi44NzQtMzMuODIzLTE1LjYtNDYuNTUtOC44MzUtOC44MzYtMjEuMDUtMjEtMjEuMDUtMjEgMi44MzMtMy42IDUuOTE3LTcuMDY3IDkuMjUtMTAuNCAyLjkzNC0yLjg2NyA1LjkzNC01LjU1IDktOC4wNUw2MS4xIDQzLjg1Qzc0LjEwMiA1Ni44NTIgOTAuNzY3IDYwLjIgMTA4LjcgNjAuMmMxOC40NjcgMCAzNS4wNzctMy41NzcgNDguNi0xNy4xIDguMzItOC4zMiAxOS4zLTE5LjI1IDE5LjMtMTkuMjUgMi45IDIuMzY3IDUuNzMzIDQuOTMzIDguNSA3LjcgMy40NjcgMy41MzMgNi42NSA3LjE4MyA5LjU1IDEwLjk1eiIgZmlsbD0iIzNBM0Y1MSIgZmlsbC1ydWxlPSJldmVub2RkIi8+CiAgPGcgY2xpcC1ydWxlPSJldmVub2RkIj4KICAgIDxwYXRoIGQ9Ik03OC43IDExNGMtLjItMS4xNjctLjMzMi0yLjM1LS40LTMuNTUtLjAzMi0uNjY3LS4wNS0xLjMzMy0uMDUtMiAwLS43LjAxOC0xLjM2Ny4wNS0yIDAtLjA2Ny4wMTgtLjEzMy4wNS0uMi40MzUtNy4zNjcgMy4zMzQtMTMuNzMzIDguNy0xOS4xIDUuOS01LjgzMyAxMi45ODQtOC43NSAyMS4yNS04Ljc1IDguMyAwIDE1LjM4NCAyLjkxNyAyMS4yNSA4Ljc1IDUuODM0IDUuOTM0IDguNzUgMTMuMDMzIDguNzUgMjEuMyAwIDguMjY3LTIuOTE2IDE1LjM1LTguNzUgMjEuMjUtLjIuMjMzLS40MTYuNDUtLjY1LjY1LS45NjYuOTMzLTEuOTgyIDEuNzgzLTMuMDUgMi41NS01LjA2NSAzLjczMy0xMC45MTYgNS42LTE3LjU1IDUuNnMtMTIuNDY2LTEuODY2LTE3LjUtNS42Yy0xLjMzMi0uOTM0LTIuNTgyLTItMy43NS0zLjItNC41MzItNC41LTcuMzE2LTkuNzM0LTguMzUtMTUuN3oiIGZpbGw9IiMwQ0YiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPgogICAgPHBhdGggZD0iTTE1Ny44IDU5Ljc1bC0xNSAxNC42NU0zMC43ODUgMzIuNTI2TDcxLjY1IDczLjI1bTg0LjYgODQuMjVsMjcuODA4IDI4Ljc4bTEuODU1LTE1My44OTRMMTU3LjggNTkuNzVtLTEyNS40NSAxMjZsMjcuMzUtMjcuNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMENGIiBzdHJva2UtbWl0ZXJsaW1pdD0iMSIgc3Ryb2tlLXdpZHRoPSIyIi8+CiAgICA8cGF0aCBkPSJNMTU3LjggNTkuNzVsLTE2Ljk1IDE3LjJNNTguOTcgNjAuNjA0bDE3LjIgMTcuMTVNNTkuNjIzIDE1OC40M2wxNi43NS0xNy40bTYxLjkyOC0xLjM5NmwxOC4wMjggMTcuOTQ1IiBmaWxsPSJub25lIiBzdHJva2U9IiMwQ0YiIHN0cm9rZS1taXRlcmxpbWl0PSIxIiBzdHJva2Utd2lkdGg9IjciLz4KICA8L2c+Cjwvc3ZnPg==';
function LoadingPage() {
return (
<div className={styles.page}>
<img
className={styles.logoFull}
src={sonarrLogo}
/>
<LoadingMessage />
<LoadingIndicator />
</div>

View File

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

View File

@@ -191,7 +191,7 @@ class TableOptionsModal extends Component {
<TableOptionsColumnDragSource
key={name}
name={name}
label={columnLabel || label}
label={label || columnLabel}
isVisible={isVisible}
isModifiable={true}
index={index}
@@ -209,7 +209,7 @@ class TableOptionsModal extends Component {
<TableOptionsColumn
key={name}
name={name}
label={columnLabel || label}
label={label || columnLabel}
isVisible={isVisible}
index={index}
isModifiable={false}

View File

@@ -14,7 +14,6 @@
&.inverse {
background-color: $themeDarkColor;
box-shadow: 0 5px 10px $popoverShadowInverseColor;
color: $white;
}
}

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

@@ -3,7 +3,6 @@ import React, { Component } from 'react';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatBytes from 'Utilities/Number/formatBytes';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Icon from 'Components/Icon';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -15,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';
@@ -116,17 +114,8 @@ class InteractiveSearchRow extends Component {
quality,
language,
preferredWordScore,
sceneMapping,
seasonNumber,
episodeNumbers,
absoluteEpisodeNumbers,
mappedSeasonNumber,
mappedEpisodeNumbers,
mappedAbsoluteEpisodeNumbers,
rejections,
episodeRequested,
downloadAllowed,
isDaily,
isGrabbing,
isGrabbed,
longDateFormat,
@@ -153,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}>
@@ -194,7 +171,8 @@ class InteractiveSearchRow extends Component {
</TableRowCell>
<TableRowCell className={styles.preferredWordScore}>
{formatPreferredWordScore(preferredWordScore)}
{preferredWordScore > 0 && `+${preferredWordScore}`}
{preferredWordScore < 0 && preferredWordScore}
</TableRowCell>
<TableRowCell className={styles.rejected}>
@@ -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>
);
})
@@ -26,7 +22,7 @@ function SeriesAlternateTitles({ alternateTitles }) {
}
SeriesAlternateTitles.propTypes = {
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired
};
export default SeriesAlternateTitles;

View File

@@ -119,13 +119,6 @@
margin: 5px 10px 5px 0;
}
.fileCountMessage {
padding: 5px;
white-space: nowrap;
font-weight: 300;
font-size: 15px;
}
.path,
.sizeOnDisk,
.qualityProfileName,

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}>
@@ -432,32 +414,22 @@ class SeriesDetails extends Component {
</span>
</Label>
<Tooltip
anchor={
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={icons.DRIVE}
size={17}
/>
<Label
className={styles.detailsLabel}
title={episodeFilesCountMessage}
size={sizes.LARGE}
>
<Icon
name={icons.DRIVE}
size={17}
/>
<span className={styles.sizeOnDisk}>
{
formatBytes(sizeOnDisk || 0)
}
</span>
</Label>
}
tooltip={
<span>
{episodeFilesCountMessage}
</span>
}
kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM}
/>
<span className={styles.sizeOnDisk}>
{
formatBytes(sizeOnDisk)
}
</span>
</Label>
<Label
className={styles.detailsLabel}
@@ -674,12 +646,6 @@ class SeriesDetails extends Component {
showImportMode={false}
onModalClose={this.onInteractiveImportModalClose}
/>
<MonitoringOptionsModal
isOpen={isMonitorOptionsModalOpen}
seriesId={id}
onModalClose={this.onMonitorOptionsClose}
/>
</PageContentBody>
</PageContent>
);
@@ -698,16 +664,14 @@ 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,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
seasons: PropTypes.arrayOf(PropTypes.object).isRequired,
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
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,

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