1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-25 17:35:07 -04:00

Compare commits

..

4 Commits

Author SHA1 Message Date
Taloth Saldono
808836a65f Help text 2020-02-15 01:30:27 +01:00
Taloth Saldono
6f7e505bed Added Enabled & IndexerId to Edit Release Profile UI 2020-02-14 23:50:50 +01:00
Jacob
ecf1d75954 ui v1 2020-02-14 23:21:22 +01:00
netpok
c5257de436 New: Added aired-before field to kodi metadata to sort specials
closes #3073
2020-02-14 22:42:24 +01:00
472 changed files with 2885 additions and 8379 deletions

View File

@@ -1,73 +1,58 @@
# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr
# Sonarr
Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
## Getting Started
## Major Features Include:
- [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
- [Donate](https://sonarr.tv/donate)
- [Discord](https://discord.gg/M6BvZn5)
- [Reddit](https://www.reddit.com/r/sonarr)
## Features
### Current Features
- Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
- Automatically detects new episodes
- Can scan your existing library and download any missing episodes
- Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
- Automatic failed download handling will try another release if one fails
- Manual search so you can pick any release or to see why a release was not downloaded automatically
- Fully configurable episode renaming
- Full integration with SABnzbd and NZBGet
- Full integration with Kodi, Plex (notification, library update, metadata)
- Full support for specials and multi-episode releases
- And a beautiful UI
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Automatically detects new episodes
* Can scan your existing library and download any missing episodes
* Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
* Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically
* Fully configurable episode renaming
* Full integration with SABnzbd and NZBGet
* Full integration with Kodi, Plex (notification, library update, metadata)
* Full support for specials and multi-episode releases
* And a beautiful UI
## Configuring Development Environment:
### 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)
* [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`
* Make sure all the required software mentioned above are installed
* Clone the repository into your development machine. [*info*](https://help.github.com/en/articles/working-with-forks)
* Grab the submodules `git submodule init && git submodule update`
* Install the required Node Packages `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
* Run `yarn build` to build the UI
* Open `Sonarr.sln` in Visual Studio
* Make sure `NzbDrone.Console` is set as the startup project
* Build `NzbDrone.Windows` and `NzbDrone.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)
* Run `yarn watch` to build UI and rebuild automatically when changes are detected
* Run Sonarr.Console.exe (or debug in Visual Studio)
### Licenses
### License
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
- Copyright 2010-2020
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2019
### Sponsors
- [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
- [ReSharper](http://www.jetbrains.com/resharper/)
- [TeamCity](http://www.jetbrains.com/teamcity/)
* [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
* [ReSharper](http://www.jetbrains.com/resharper/)
* [TeamCity](http://www.jetbrains.com/teamcity/)

View File

@@ -7,8 +7,8 @@ Vcs-Git: git@github.com:Sonarr/Sonarr.git
Vcs-Browser: https://github.com/Sonarr/Sonarr
Build-Depends: debhelper (>= 9),
dh-systemd (>= 1.5),
mono-devel (>= 5.18),
libmono-cil-dev (>= 5.18),
mono-devel (>= 5.4),
libmono-cil-dev (>= 5.4),
cli-common-dev (>= 0.9+xamarin5)
Package: sonarr
@@ -16,7 +16,7 @@ Architecture: all
Provides: nzbdrone
Conflicts: nzbdrone
Replaces: nzbdrone
Depends: adduser, libsqlite3-0 (>= 3.7), libmediainfo0v5 (>= 0.7.52) | libmediainfo0 (>= 0.7.52), mono-runtime (>= 5.18), ca-certificates-mono, libmono-system-net-http4.0-cil (>= 4.0.0~alpha1), ${cli:Depends}, ${misc:Depends}
Depends: adduser, libsqlite3-0 (>= 3.7), libmediainfo0v5 (>= 0.7.52) | libmediainfo0 (>= 0.7.52), mono-runtime (>= 5.4), ca-certificates-mono, libmono-system-net-http4.0-cil (>= 4.0.0~alpha1), ${cli:Depends}, ${misc:Depends}
Recommends: libmediainfo0v5 (>= 18.03) | libmediainfo0 (>= 18.03)
Suggests: sqlite3 (>= 3.7), mediainfo (>= 0.7.52)
Description: Internet PVR

View File

@@ -95,7 +95,7 @@ chown -R $USER:$GROUP /usr/lib/sonarr
sed -i "s:User=sonarr:User=$USER:g; s:Group=sonarr:Group=$GROUP:g; s:-data=/var/lib/sonarr:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
#BEGIN BUILTIN UPDATER
if [ "$UPDATER" = "BuiltIn" ]; then
if [ $1 = "upgrade" ] && [ "$UPDATER" = "BuiltIn" ]; then
# If we upgraded, signal Sonarr to do an update check on startup instead of scheduled.
touch $CONFDIR/update_required
chown $USER:$GROUP $CONFDIR/update_required

View File

@@ -3,7 +3,7 @@
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll libmonosgen-2.0 clr mscorlib mscoree.dll Microsoft.DiaSymReader.Native.x86.dll Microsoft.DiaSymReader.Native.amd64.dll
EXCLUDE_MODULEREFS = crypt32 httpapi
%:
dh $@ --with=systemd --with=cli

View File

@@ -1,7 +1,7 @@
FROM ubuntu:xenial AS builder
ENV DEBIAN_FRONTEND noninteractive
ENV MONO_VERSION 5.18
ENV MONO_VERSION 5.14
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
echo "deb http://download.mono-project.com/repo/debian stable-xenial/snapshots/$MONO_VERSION main" > /etc/apt/sources.list.d/mono-official-stable.list && \

View File

@@ -16,7 +16,6 @@ RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E03280
RUN apt-get update && apt-get install -y \
libmono-system-runtime4.0-cil \
libmono-system-net-http4.0-cil \
&& rm -rf /var/lib/apt/lists/*
COPY startup.sh /startup.sh

View File

@@ -2,18 +2,16 @@
opt_parallel=
opt_version=
opt_mode=both
while getopts 'pv:m:r?h' c
while getopts 'pv:m:?h' c
do
case $c in
p) opt_parallel=1 ;;
v) opt_version=$OPTARG ;;
m) opt_mode=$OPTARG ;;
r) opt_report=1 ;;
?|h) printf "Usage: %s [-p] [-v mono-ver] [-m sonarr|complete]\n" $0
printf " -p run parallel\n"
printf " -v run specified mono version\n"
printf " -m run only mono-'complete' or 'sonarr' package variants\n"
printf " -r only report\n"
exit 2
esac
done
@@ -22,22 +20,17 @@ done
# make sure that the docker host has enough memory to handle about ~300 MB per container, so 2-3 GB total
# excess goes to the swap and will slow down the entire system
MONO_VERSIONS=""
# Preferred versions
MONO_VERSIONS="6.8 6.6 6.4 6.0 5.20 5.18"
# Future versions
MONO_VERSIONS="$MONO_VERSIONS 6.10=preview-xenial"
# Semi-Supported versions
MONO_VERSIONS="$MONO_VERSIONS 6.8 6.6 6.4 6.0"
# Supported versions
MONO_VERSIONS="$MONO_VERSIONS 5.20 5.18"
# Legacy unsupported versions (but appear to work)
MONO_VERSIONS="$MONO_VERSIONS 5.16 5.14 5.12"
MONO_VERSIONS="$MONO_VERSIONS 5.16 5.14 5.12 5.10 5.8 5.4"
# Legacy unsupported versions
MONO_VERSIONS="$MONO_VERSIONS 5.10 5.8 5.4 5.0"
MONO_VERSIONS="$MONO_VERSIONS 5.0"
#MONO_VERSIONS="$MONO_VERSIONS 4.8=stable-wheezy/snapshots/4.8"
if [ "$opt_version" != "" ]; then
@@ -93,29 +86,23 @@ runOne() {
echo "Finished Test Docker for mono $MONO_VERSION"
}
if [ "$opt_report" != "1" ]; then
if [ "$opt_parallel" == "1" ]; then
for MONO_VERSION_PAIR in $MONO_VERSIONS; do
prepOne "$MONO_VERSION_PAIR"
done
fi
if [ "$opt_parallel" == "1" ]; then
for MONO_VERSION_PAIR in $MONO_VERSIONS; do
if [ "$opt_parallel" == "1" ]; then
runOne "$MONO_VERSION_PAIR" &
else
prepOne "$MONO_VERSION_PAIR"
runOne "$MONO_VERSION_PAIR"
fi
prepOne "$MONO_VERSION_PAIR"
done
if [ "$opt_parallel" == "1" ]; then
echo "Waiting for all runs to finish"
wait
echo "Finished all runs"
fi
fi
grep "<test-run" ../../_tests_results/**/*.xml | sed -r 's/.*?mono-([0-9.]+(-s)?).*?_([IU]).*?\.xml.*?failed="([0-9]*)".*/\1\t\3:\tfailed \4/g' | sort -V -t.
for MONO_VERSION_PAIR in $MONO_VERSIONS; do
if [ "$opt_parallel" == "1" ]; then
runOne "$MONO_VERSION_PAIR" &
else
prepOne "$MONO_VERSION_PAIR"
runOne "$MONO_VERSION_PAIR"
fi
done
if [ "$opt_parallel" == "1" ]; then
echo "Waiting for all runs to finish"
wait
echo "Finished all runs"
fi

View File

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

View File

@@ -5,6 +5,17 @@ const cache = require('gulp-cached');
const livereload = require('gulp-livereload');
const paths = require('./helpers/paths.js');
gulp.task('copyJs', () => {
return gulp.src(
[
path.join(paths.src.root, 'polyfills.js')
], { base: paths.src.root })
.pipe(cache('copyJs'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});
gulp.task('copyHtml', () => {
return gulp.src(paths.src.html, { base: paths.src.root })
.pipe(cache('copyHtml'))

View File

@@ -6,21 +6,17 @@ const webpack = require('webpack');
const errorHandler = require('./helpers/errorHandler');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const uiFolder = 'UI';
const frontendFolder = path.join(__dirname, '..');
const srcFolder = path.join(frontendFolder, 'src');
const isProduction = process.argv.indexOf('--production') > -1;
const isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
const inlineWebWorkers = true;
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
console.log('Source Folder:', srcFolder);
console.log('Output Folder:', distFolder);
console.log('isProduction:', isProduction);
console.log('isProfiling:', isProfiling);
const cssVarsFiles = [
'../src/Styles/Variables/colors',
@@ -117,22 +113,6 @@ const config = {
module: {
rules: [
{
test: /\.worker\.js$/,
issuer: {
// monaco-editor includes the editor.worker.js in other language workers,
// don't use worker-loader in that case
exclude: /monaco-editor/
},
use: {
loader: 'worker-loader',
options: {
name: '[name].js',
inline: inlineWebWorkers,
fallback: !inlineWebWorkers
}
}
},
{
test: /\.js?$/,
exclude: /(node_modules|JsLibraries)/,
@@ -233,24 +213,6 @@ const config = {
}
};
if (isProfiling) {
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
config.optimization.minimizer = [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
mangle: false,
keep_classnames: true,
keep_fnames: true
}
})
];
}
gulp.task('webpack', () => {
return webpackStream(config)
.pipe(gulp.dest('_output/UI'));

View File

@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"checkJs": false,
"baseUrl": "src",
"jsx": "react",
"module": "commonjs",
"moduleResolution": "node",
"paths": {
"*": [
"*"
]
}
},
"include": [
"./src/**/*"
],
"exclude": [
]
}

View File

@@ -7,7 +7,7 @@ import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
@@ -56,7 +56,7 @@ class Blacklist extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
<PageContentBodyConnector>
{
isFetching && !isPopulated &&
<LoadingIndicator />
@@ -103,7 +103,7 @@ class Blacklist extends Component {
/>
</div>
}
</PageContentBody>
</PageContentBodyConnector>
</PageContent>
);
}

View File

@@ -8,7 +8,7 @@ import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
@@ -96,7 +96,7 @@ class History extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
<PageContentBodyConnector>
{
isFetchingAny && !isAllPopulated &&
<LoadingIndicator />
@@ -147,7 +147,7 @@ class History extends Component {
/>
</div>
}
</PageContentBody>
</PageContentBodyConnector>
</PageContent>
);
}

View File

@@ -1,7 +1,6 @@
import _ from 'lodash';
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';
@@ -13,7 +12,7 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
@@ -37,26 +36,34 @@ class Queue extends Component {
lastToggled: null,
selectedState: {},
isPendingSelected: false,
isConfirmRemoveModalOpen: false,
items: props.items
isConfirmRemoveModalOpen: false
};
}
componentDidUpdate(prevProps) {
const {
items,
isEpisodesFetching
} = this.props;
shouldComponentUpdate(nextProps) {
// Don't update when fetching has completed if items have changed,
// before episodes start fetching or when episodes start fetching.
if (
(!isEpisodesFetching && prevProps.isEpisodesFetching) ||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.episodeId))
this.props.isFetching &&
nextProps.isPopulated &&
hasDifferentItems(this.props.items, nextProps.items) &&
nextProps.items.some((e) => e.episodeId)
) {
return false;
}
if (!this.props.isEpisodesFetching && nextProps.isEpisodesFetching) {
return false;
}
return true;
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
this.setState((state) => {
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
return removeOldSelectedState(state, prevProps.items);
});
return;
@@ -117,6 +124,7 @@ class Queue extends Component {
isFetching,
isPopulated,
error,
items,
isEpisodesFetching,
isEpisodesPopulated,
episodesError,
@@ -134,8 +142,7 @@ class Queue extends Component {
allUnselected,
selectedState,
isConfirmRemoveModalOpen,
isPendingSelected,
items
isPendingSelected
} = this.state;
const isRefreshing = isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
@@ -191,7 +198,7 @@ class Queue extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
<PageContentBodyConnector>
{
isRefreshing && !isAllPopulated &&
<LoadingIndicator />
@@ -248,7 +255,7 @@ class Queue extends Component {
/>
</div>
}
</PageContentBody>
</PageContentBodyConnector>
<RemoveQueueItemsModal
isOpen={isConfirmRemoveModalOpen}

View File

@@ -10,13 +10,13 @@ function QueueDetails(props) {
size,
sizeleft,
estimatedCompletionTime,
status,
trackedDownloadState,
trackedDownloadStatus,
status: queueStatus,
errorMessage,
progressBar
} = props;
const status = queueStatus.toLowerCase();
const progress = (100 - sizeleft / size * 100);
if (status === 'pending') {
@@ -39,35 +39,7 @@ function QueueDetails(props) {
);
}
if (trackedDownloadStatus === 'warning') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.WARNING}
title={'Downloaded - Unable to Import: check logs for details'}
/>
);
}
if (trackedDownloadState === 'importPending') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={'Downloaded - Waiting to Import'}
/>
);
}
if (trackedDownloadState === 'importing') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={'Downloaded - Importing'}
/>
);
}
// TODO: show an icon when download is complete, but not imported yet?
}
if (errorMessage) {
@@ -118,8 +90,6 @@ QueueDetails.propTypes = {
sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
progressBar: PropTypes.node.isRequired
};

View File

@@ -222,13 +222,9 @@ class QueueRow extends Component {
if (name === 'quality') {
return (
<TableRowCell key={name}>
{
quality ?
<EpisodeQuality
quality={quality}
/> :
null
}
<EpisodeQuality
quality={quality}
/>
</TableRowCell>
);
}
@@ -356,7 +352,7 @@ class QueueRow extends Component {
<RemoveQueueItemModal
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canIgnore={!!series}
canIgnore={!!(series && episode)}
onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose}
/>

View File

@@ -1,3 +1,4 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -14,11 +15,11 @@ function createMapStateToProps() {
createEpisodeSelector(),
createUISettingsSelector(),
(series, episode, uiSettings) => {
const result = {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};
const result = _.pick(uiSettings, [
'showRelativeDates',
'shortDateFormat',
'timeFormat'
]);
result.series = series;
result.episode = episode;

View File

@@ -51,6 +51,10 @@ function QueueStatusCell(props) {
let iconKind = kinds.DEFAULT;
let title = 'Downloading';
if (hasWarning) {
iconKind = kinds.WARNING;
}
if (status === 'paused') {
iconName = icons.PAUSED;
title = 'Paused';
@@ -67,24 +71,17 @@ function QueueStatusCell(props) {
if (trackedDownloadState === 'importPending') {
title += ' - Waiting to Import';
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'importing') {
title += ' - Importing';
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'failedPending') {
title += ' - Waiting to Process';
iconKind = kinds.DANGER;
}
}
if (hasWarning) {
iconKind = kinds.WARNING;
}
if (status === 'delay') {
iconName = icons.PENDING;
title = 'Pending';

View File

@@ -19,7 +19,7 @@ function TimeleftCell(props) {
timeFormat
} = props;
if (status === 'delay') {
if (status === 'Delay') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
@@ -33,7 +33,7 @@ function TimeleftCell(props) {
);
}
if (status === 'downloadClientUnavailable') {
if (status === 'DownloadClientUnavailable') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
@@ -47,7 +47,7 @@ function TimeleftCell(props) {
);
}
if (!timeleft || status === 'completed' || status === 'failed') {
if (!timeleft) {
return (
<TableRowCell className={styles.timeleft}>
-

View File

@@ -8,7 +8,7 @@ import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import TextInput from 'Components/Form/TextInput';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import AddNewSeriesSearchResultConnector from './AddNewSeriesSearchResultConnector';
import styles from './AddNewSeries.css';
@@ -88,7 +88,7 @@ class AddNewSeries extends Component {
return (
<PageContent title="Add New Series">
<PageContentBody>
<PageContentBodyConnector>
<div className={styles.searchContainer}>
<div className={styles.searchIconContainer}>
<Icon
@@ -191,7 +191,7 @@ class AddNewSeries extends Component {
}
<div />
</PageContentBody>
</PageContentBodyConnector>
</PageContent>
);
}

View File

@@ -62,7 +62,6 @@
.alreadyExistsIcon {
margin-left: 10px;
color: #37bc9b;
pointer-events: all;
}
.overview {

View File

@@ -5,7 +5,7 @@ import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import ImportSeriesTableConnector from './ImportSeriesTableConnector';
import ImportSeriesFooterConnector from './ImportSeriesFooterConnector';
@@ -21,15 +21,17 @@ class ImportSeries extends Component {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {}
selectedState: {},
contentBody: null,
scrollTop: 0
};
}
//
// Control
setScrollerRef = (ref) => {
this.setState({ scroller: ref });
setContentBodyRef = (ref) => {
this.setState({ contentBody: ref });
}
//
@@ -92,13 +94,13 @@ class ImportSeries extends Component {
allSelected,
allUnselected,
selectedState,
scroller
contentBody
} = this.state;
return (
<PageContent title="Import Series">
<PageContentBody
registerScroller={this.setScrollerRef}
<PageContentBodyConnector
ref={this.setContentBodyRef}
onScroll={this.onScroll}
>
{
@@ -119,21 +121,23 @@ class ImportSeries extends Component {
}
{
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && scroller &&
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && contentBody &&
<ImportSeriesTableConnector
rootFolderId={rootFolderId}
unmappedFolders={unmappedFolders}
allSelected={allSelected}
allUnselected={allUnselected}
selectedState={selectedState}
scroller={scroller}
contentBody={contentBody}
showLanguageProfile={showLanguageProfile}
scrollTop={this.state.scrollTop}
onSelectAllChange={this.onSelectAllChange}
onSelectedChange={this.onSelectedChange}
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
onScroll={this.onScroll}
/>
}
</PageContentBody>
</PageContentBodyConnector>
{
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length &&

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import { inputTypes } from 'Helpers/Props';
import FormInputGroup from 'Components/Form/FormInputGroup';
import VirtualTableRow from 'Components/Table/VirtualTableRow';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import ImportSeriesSelectSeriesConnector from './SelectSeries/ImportSeriesSelectSeriesConnector';
@@ -9,6 +10,7 @@ import styles from './ImportSeriesRow.css';
function ImportSeriesRow(props) {
const {
style,
id,
monitor,
qualityProfileId,
@@ -24,7 +26,7 @@ function ImportSeriesRow(props) {
} = props;
return (
<>
<VirtualTableRow style={style}>
<VirtualTableSelectCell
inputClassName={styles.selectInput}
id={id}
@@ -91,11 +93,12 @@ function ImportSeriesRow(props) {
onInputChange={onInputChange}
/>
</VirtualTableRowCell>
</>
</VirtualTableRow>
);
}
ImportSeriesRow.propTypes = {
style: PropTypes.object.isRequired,
id: PropTypes.string.isRequired,
monitor: PropTypes.string.isRequired,
qualityProfileId: PropTypes.number.isRequired,

View File

@@ -2,7 +2,6 @@ import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import VirtualTable from 'Components/Table/VirtualTable';
import VirtualTableRow from 'Components/Table/VirtualTableRow';
import ImportSeriesHeader from './ImportSeriesHeader';
import ImportSeriesRowConnector from './ImportSeriesRowConnector';
@@ -113,19 +112,15 @@ class ImportSeriesTable extends Component {
const item = items[rowIndex];
return (
<VirtualTableRow
<ImportSeriesRowConnector
key={key}
style={style}
>
<ImportSeriesRowConnector
key={item.id}
rootFolderId={rootFolderId}
showLanguageProfile={showLanguageProfile}
isSelected={selectedState[item.id]}
onSelectedChange={onSelectedChange}
id={item.id}
/>
</VirtualTableRow>
rootFolderId={rootFolderId}
showLanguageProfile={showLanguageProfile}
isSelected={selectedState[item.id]}
onSelectedChange={onSelectedChange}
id={item.id}
/>
);
}
@@ -138,10 +133,12 @@ class ImportSeriesTable extends Component {
allSelected,
allUnselected,
isSmallScreen,
scroller,
contentBody,
showLanguageProfile,
scrollTop,
selectedState,
onSelectAllChange
onSelectAllChange,
onScroll
} = this.props;
if (!items.length) {
@@ -151,9 +148,10 @@ class ImportSeriesTable extends Component {
return (
<VirtualTable
items={items}
scroller={scroller}
contentBody={contentBody}
isSmallScreen={isSmallScreen}
rowHeight={52}
scrollTop={scrollTop}
overscanRowCount={2}
rowRenderer={this.rowRenderer}
header={
@@ -165,6 +163,7 @@ class ImportSeriesTable extends Component {
/>
}
selectedState={selectedState}
onScroll={onScroll}
/>
);
}
@@ -184,13 +183,15 @@ ImportSeriesTable.propTypes = {
selectedState: PropTypes.object.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
allSeries: PropTypes.arrayOf(PropTypes.object),
scroller: PropTypes.instanceOf(Element).isRequired,
contentBody: PropTypes.object.isRequired,
showLanguageProfile: PropTypes.bool.isRequired,
scrollTop: PropTypes.number.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemoveSelectedStateItem: PropTypes.func.isRequired,
onSeriesLookup: PropTypes.func.isRequired,
onSetImportSeriesValue: PropTypes.func.isRequired
onSetImportSeriesValue: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired
};
export default ImportSeriesTable;

View File

@@ -7,7 +7,7 @@ import FieldSet from 'Components/FieldSet';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import RootFolders from 'RootFolder/RootFolders';
import styles from './ImportSeriesSelectFolder.css';
@@ -53,7 +53,7 @@ class ImportSeriesSelectFolder extends Component {
return (
<PageContent title="Import Series">
<PageContentBody>
<PageContentBodyConnector>
{
isFetching && !isPopulated &&
<LoadingIndicator />
@@ -132,7 +132,7 @@ class ImportSeriesSelectFolder extends Component {
/>
</div>
}
</PageContentBody>
</PageContentBodyConnector>
</PageContent>
);
}

View File

@@ -33,7 +33,6 @@ import BackupsConnector from 'System/Backup/BackupsConnector';
import UpdatesConnector from 'System/Updates/UpdatesConnector';
import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs';
import Diagnostic from 'Diagnostic/Diagnostic';
function AppRoutes(props) {
const {
@@ -230,15 +229,6 @@ function AppRoutes(props) {
component={Logs}
/>
{/*
Diagnostics
*/}
<Route
path="/diag"
component={Diagnostic}
/>
{/*
Not Found
*/}

View File

@@ -3,10 +3,9 @@ import React, { Component } from 'react';
import { align, icons } from 'Helpers/Props';
import PageContent from 'Components/Page/PageContent';
import Measure from 'Components/Measure';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import FilterMenu from 'Components/Menu/FilterMenu';
import NoSeries from 'Series/NoSeries';
@@ -77,10 +76,8 @@ class CalendarPage extends Component {
filters,
hasSeries,
missingEpisodeIds,
isRssSyncExecuting,
isSearchingForMissing,
useCurrentPage,
onRssSyncPress,
onFilterSelect
} = this.props;
@@ -102,15 +99,6 @@ class CalendarPage extends Component {
onPress={this.onGetCalendarLinkPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label="RSS Sync"
iconName={icons.RSS}
isSpinning={isRssSyncExecuting}
onPress={onRssSyncPress}
/>
<PageToolbarButton
label="Search for Missing"
iconName={icons.SEARCH}
@@ -138,7 +126,7 @@ class CalendarPage extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBody
<PageContentBodyConnector
className={styles.calendarPageBody}
innerClassName={styles.calendarInnerPageBody}
>
@@ -159,7 +147,7 @@ class CalendarPage extends Component {
hasSeries &&
<LegendConnector />
}
</PageContentBody>
</PageContentBodyConnector>
<CalendarLinkModal
isOpen={isCalendarLinkModalOpen}
@@ -180,12 +168,10 @@ CalendarPage.propTypes = {
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
hasSeries: PropTypes.bool.isRequired,
missingEpisodeIds: PropTypes.arrayOf(PropTypes.number).isRequired,
isRssSyncExecuting: PropTypes.bool.isRequired,
isSearchingForMissing: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired,
onSearchMissingPress: PropTypes.func.isRequired,
onDaysCountChange: PropTypes.func.isRequired,
onRssSyncPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired
};

View File

@@ -3,14 +3,11 @@ import { createSelector } from 'reselect';
import moment from 'moment';
import { isCommandExecuting } from 'Utilities/Command';
import isBefore from 'Utilities/Date/isBefore';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { executeCommand } from 'Store/Actions/commandActions';
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
import createSeriesCountSelector from 'Store/Selectors/createSeriesCountSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import CalendarPage from './CalendarPage';
function createMissingEpisodeIdsSelector() {
@@ -62,7 +59,6 @@ function createMapStateToProps() {
createSeriesCountSelector(),
createUISettingsSelector(),
createMissingEpisodeIdsSelector(),
createCommandExecutingSelector(commandNames.RSS_SYNC),
createIsSearchingSelector(),
(
selectedFilterKey,
@@ -70,7 +66,6 @@ function createMapStateToProps() {
seriesCount,
uiSettings,
missingEpisodeIds,
isRssSyncExecuting,
isSearchingForMissing
) => {
return {
@@ -79,7 +74,6 @@ function createMapStateToProps() {
colorImpairedMode: uiSettings.enableColorImpairedMode,
hasSeries: !!seriesCount,
missingEpisodeIds,
isRssSyncExecuting,
isSearchingForMissing
};
}
@@ -88,12 +82,6 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
onRssSyncPress() {
dispatch(executeCommand({
name: commandNames.RSS_SYNC
}));
},
onSearchMissingPress(episodeIds) {
dispatch(searchMissing({ episodeIds }));
},

View File

@@ -11,12 +11,10 @@ function CalendarEventQueueDetails(props) {
sizeleft,
estimatedCompletionTime,
status,
trackedDownloadState,
trackedDownloadStatus,
errorMessage
} = props;
const progress = size ? (100 - sizeleft / size * 100) : 0;
const progress = (100 - sizeleft / size * 100);
return (
<QueueDetails
@@ -25,8 +23,6 @@ function CalendarEventQueueDetails(props) {
sizeleft={sizeleft}
estimatedCompletionTime={estimatedCompletionTime}
status={status}
trackedDownloadState={trackedDownloadState}
trackedDownloadStatus={trackedDownloadStatus}
errorMessage={errorMessage}
progressBar={
<div title={`Episode is downloading - ${progress.toFixed(1)}% ${title}`}>
@@ -48,8 +44,6 @@ CalendarEventQueueDetails.propTypes = {
sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
errorMessage: PropTypes.string
};

View File

@@ -172,7 +172,7 @@ class FilterBuilderModalContent extends Component {
filters.map((filter, index) => {
return (
<FilterBuilderRow
key={`${filter.key}-${index}`}
key={index}
index={index}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}

View File

@@ -135,7 +135,7 @@ class FilterBuilderRowValue extends Component {
tagList={tagList}
allowNew={!tagList.length}
kind={kinds.DEFAULT}
delimiters={['Tab', 'Enter']}
delimiters={[9, 13]}
maxSuggestionsLength={100}
minQueryLength={0}
tagComponent={FilterBuilderRowValueTag}

View File

@@ -1,8 +1,19 @@
.enhancedSelect {
composes: input from '~Components/Form/Input.css';
composes: link from '~Components/Link/Link.css';
position: relative;
display: flex;
align-items: center;
padding: 6px 16px;
width: 100%;
height: 35px;
border: 1px solid $inputBorderColor;
border-radius: 4px;
background-color: $white;
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
color: $black;
cursor: default;
}
.hasError {
@@ -45,7 +56,6 @@
display: flex;
justify-content: center;
max-width: 90%;
max-height: 100%;
width: 350px !important;
height: auto !important;
}

View File

@@ -56,6 +56,16 @@ class IndexerSelectInputConnector extends Component {
if (!this.props.isPopulated) {
this.props.dispatchFetchIndexers();
}
const {
name,
value,
values
} = this.props;
if (!value || !_.some(values, (option) => parseInt(option.key) === value)) {
this.onChange({ name, value: 0 });
}
}
//

View File

@@ -98,9 +98,7 @@ class KeyValueListInput extends Component {
className,
value,
keyPlaceholder,
valuePlaceholder,
hasError,
hasWarning
valuePlaceholder
} = this.props;
const { isFocused } = this.state;
@@ -108,9 +106,7 @@ class KeyValueListInput extends Component {
return (
<div className={classNames(
className,
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
isFocused && styles.isFocused
)}
>
{

View File

@@ -8,16 +8,7 @@
}
}
.inputWrapper {
flex: 1 0 0;
}
.buttonWrapper {
flex: 0 0 22px;
}
.keyInput,
.valueInput {
width: 100%;
border: none;
}

View File

@@ -63,41 +63,34 @@ class KeyValueListInputItem extends Component {
return (
<div className={styles.itemContainer}>
<div className={styles.inputWrapper}>
<TextInput
className={styles.keyInput}
name="key"
value={keyValue}
placeholder={keyPlaceholder}
onChange={this.onKeyChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
</div>
<TextInput
className={styles.keyInput}
name="key"
value={keyValue}
placeholder={keyPlaceholder}
onChange={this.onKeyChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
<div className={styles.inputWrapper}>
<TextInput
className={styles.valueInput}
name="value"
value={value}
placeholder={valuePlaceholder}
onChange={this.onValueChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
</div>
<TextInput
className={styles.valueInput}
name="value"
value={value}
placeholder={valuePlaceholder}
onChange={this.onValueChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
<div className={styles.buttonWrapper}>
{
isNew ?
null :
<IconButton
name={icons.REMOVE}
tabIndex={-1}
onPress={this.onRemovePress}
/>
}
</div>
{
!isNew &&
<IconButton
name={icons.REMOVE}
tabIndex={-1}
onPress={this.onRemovePress}
/>
}
</div>
);
}

View File

@@ -4,16 +4,15 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import sortByName from 'Utilities/Array/sortByName';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import SelectInput from './SelectInput';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.languageProfiles', sortByName),
(state) => state.settings.languageProfiles,
(state, { includeNoChange }) => includeNoChange,
(state, { includeMixed }) => includeMixed,
(languageProfiles, includeNoChange, includeMixed) => {
const values = _.map(languageProfiles.items, (languageProfile) => {
const values = _.map(languageProfiles.items.sort(sortByName), (languageProfile) => {
return {
key: languageProfile.id,
value: languageProfile.name

View File

@@ -4,16 +4,15 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import sortByName from 'Utilities/Array/sortByName';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import SelectInput from './SelectInput';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.qualityProfiles', sortByName),
(state) => state.settings.qualityProfiles,
(state, { includeNoChange }) => includeNoChange,
(state, { includeMixed }) => includeMixed,
(qualityProfiles, includeNoChange, includeMixed) => {
const values = _.map(qualityProfiles.items, (qualityProfile) => {
const values = _.map(qualityProfiles.items.sort(sortByName), (qualityProfile) => {
return {
key: qualityProfile.id,
value: qualityProfile.name

View File

@@ -12,14 +12,6 @@
}
}
.hasError {
composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
composes: hasWarning from '~Components/Form/Input.css';
}
.internalInput {
flex: 1 1 0%;
margin-left: 3px;

View File

@@ -100,9 +100,9 @@ class TagInput extends Component {
suggestions
} = this.state;
const key = event.key;
const keyCode = event.keyCode;
if (key === 'Backspace' && !value.length) {
if (keyCode === 8 && !value.length) {
const index = tags.length - 1;
if (index >= 0) {
@@ -116,7 +116,7 @@ class TagInput extends Component {
event.preventDefault();
}
if (delimiters.includes(key)) {
if (delimiters.includes(keyCode)) {
const selectedIndex = this._autosuggestRef.highlightedSuggestionIndex;
const tag = getTag(value, selectedIndex, suggestions, allowNew);
@@ -210,8 +210,6 @@ class TagInput extends Component {
const {
className,
inputContainerClassName,
hasError,
hasWarning,
...otherProps
} = this.props;
@@ -228,9 +226,7 @@ class TagInput extends Component {
className={styles.internalInput}
inputContainerClassName={classNames(
inputContainerClassName,
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
isFocused && styles.isFocused
)}
value={value}
suggestions={suggestions}
@@ -260,7 +256,7 @@ TagInput.propTypes = {
allowNew: PropTypes.bool.isRequired,
kind: PropTypes.oneOf(kinds.all).isRequired,
placeholder: PropTypes.string.isRequired,
delimiters: PropTypes.arrayOf(PropTypes.string).isRequired,
delimiters: PropTypes.arrayOf(PropTypes.number).isRequired,
minQueryLength: PropTypes.number.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
@@ -275,7 +271,8 @@ TagInput.defaultProps = {
allowNew: true,
kind: kinds.INFO,
placeholder: '',
delimiters: ['Tab', 'Enter', ' ', ','],
// Tab, enter, space and comma
delimiters: [9, 13, 32, 188],
minQueryLength: 1,
tagComponent: TagInputTag
};

View File

@@ -25,7 +25,3 @@
.warning {
color: $warningColor;
}
.purple {
color: $purple;
}

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import styles from './LoadingIndicator.css';
function LoadingIndicator({ className, rippleClassName, size }) {
function LoadingIndicator({ className, size }) {
const sizeInPx = `${size}px`;
const width = sizeInPx;
const height = sizeInPx;
@@ -17,17 +17,17 @@ function LoadingIndicator({ className, rippleClassName, size }) {
style={{ width, height }}
>
<div
className={rippleClassName}
className={styles.ripple}
style={{ width, height }}
/>
<div
className={rippleClassName}
className={styles.ripple}
style={{ width, height }}
/>
<div
className={rippleClassName}
className={styles.ripple}
style={{ width, height }}
/>
</div>
@@ -37,13 +37,11 @@ function LoadingIndicator({ className, rippleClassName, size }) {
LoadingIndicator.propTypes = {
className: PropTypes.string,
rippleClassName: PropTypes.string,
size: PropTypes.number
};
LoadingIndicator.defaultProps = {
className: styles.loading,
rippleClassName: styles.ripple,
size: 50
};

View File

@@ -1,45 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
class InlineMarkdown extends Component {
//
// Render
render() {
const {
className,
data
} = this.props;
// For now only replace links
const markdownBlocks = [];
if (data) {
const regex = RegExp(/\[(.+?)\]\((.+?)\)/g);
let endIndex = 0;
let match = null;
while ((match = regex.exec(data)) !== null) {
if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
endIndex = match.index + match[0].length;
}
if (endIndex !== data.length) {
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
}
}
return <span className={className}>{markdownBlocks}</span>;
}
}
InlineMarkdown.propTypes = {
className: PropTypes.string,
data: PropTypes.string
};
export default InlineMarkdown;

View File

@@ -1,7 +1,6 @@
import PropTypes from 'prop-types';
import React, { useEffect } from 'react';
import React from 'react';
import { kinds, sizes } from 'Helpers/Props';
import keyboardShortcuts from 'Components/keyboardShortcuts';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import Modal from 'Components/Modal/Modal';
@@ -22,19 +21,9 @@ function ConfirmModal(props) {
hideCancelButton,
isSpinning,
onConfirm,
onCancel,
bindShortcut,
unbindShortcut
onCancel
} = props;
useEffect(() => {
if (isOpen) {
bindShortcut('enter', onConfirm);
} else {
unbindShortcut('enter', onConfirm);
}
}, [onConfirm]);
return (
<Modal
isOpen={isOpen}
@@ -60,7 +49,7 @@ function ConfirmModal(props) {
}
<SpinnerButton
autoFocus={true}
data-autofocus={true}
kind={kind}
isSpinning={isSpinning}
onPress={onConfirm}
@@ -85,9 +74,7 @@ ConfirmModal.propTypes = {
hideCancelButton: PropTypes.bool,
isSpinning: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
bindShortcut: PropTypes.func.isRequired,
unbindShortcut: PropTypes.func.isRequired
onCancel: PropTypes.func.isRequired
};
ConfirmModal.defaultProps = {
@@ -98,4 +85,4 @@ ConfirmModal.defaultProps = {
isSpinning: false
};
export default keyboardShortcuts(ConfirmModal);
export default ConfirmModal;

View File

@@ -1,7 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import FocusLock from 'react-focus-lock';
import classNames from 'classnames';
import elementClass from 'element-class';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
@@ -182,33 +181,31 @@ class Modal extends Component {
}
return ReactDOM.createPortal(
<FocusLock disabled={false}>
<div
className={styles.modalContainer}
>
<div
className={styles.modalContainer}
ref={this._setBackgroundRef}
className={backdropClassName}
onMouseDown={this.onBackdropBeginPress}
onMouseUp={this.onBackdropEndPress}
>
<div
ref={this._setBackgroundRef}
className={backdropClassName}
onMouseDown={this.onBackdropBeginPress}
onMouseUp={this.onBackdropEndPress}
className={classNames(
className,
styles[size]
)}
style={style}
>
<div
className={classNames(
className,
styles[size]
)}
style={style}
<ErrorBoundary
errorComponent={ModalError}
onModalClose={onModalClose}
>
<ErrorBoundary
errorComponent={ModalError}
onModalClose={onModalClose}
>
{children}
</ErrorBoundary>
</div>
{children}
</ErrorBoundary>
</div>
</div>
</FocusLock>,
</div>,
this._node
);
}

View File

@@ -3,18 +3,6 @@
align-items: center;
}
.loading {
margin-top: 18px;
margin-bottom: 18px;
text-align: center;
}
.ripple {
composes: ripple from '~Components/Loading/LoadingIndicator.css';
border: 2px solid $toolbarColor;
}
.input {
margin-left: 8px;
width: 200px;

View File

@@ -1,18 +1,29 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
import Fuse from 'fuse.js';
import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon';
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
import SeriesSearchResult from './SeriesSearchResult';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FuseWorker from './fuse.worker';
import styles from './SeriesSearchInput.css';
const LOADING_TYPE = 'suggestionsLoading';
const ADD_NEW_TYPE = 'addNew';
const workerInstance = new FuseWorker();
const fuseOptions = {
shouldSort: true,
includeMatches: true,
threshold: 0.3,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
'title',
'alternateTitles.title',
'tags.label'
]
};
class SeriesSearchInput extends Component {
@@ -32,7 +43,6 @@ class SeriesSearchInput extends Component {
componentDidMount() {
this.props.bindShortcut(shortcuts.SERIES_SEARCH_INPUT.key, this.focusInput);
workerInstance.addEventListener('message', this.onSuggestionsReceived, false);
}
//
@@ -72,16 +82,6 @@ class SeriesSearchInput extends Component {
);
}
if (item.type === LOADING_TYPE) {
return (
<LoadingIndicator
className={styles.loading}
rippleClassName={styles.ripple}
size={30}
/>
);
}
return (
<SeriesSearchResult
{...item.item}
@@ -154,30 +154,35 @@ class SeriesSearchInput extends Component {
}
onSuggestionsFetchRequested = ({ value }) => {
this.setState({
suggestions: [
{
type: LOADING_TYPE,
title: value
const { series } = this.props;
let suggestions = [];
if (value.length === 1) {
suggestions = series.reduce((acc, s) => {
if (s.firstCharacter === value.toLowerCase()) {
acc.push({
item: s,
indices: [
[0, 0]
],
matches: [
{
value: s.title,
key: 'title'
}
],
arrayIndex: 0
});
}
]
});
this.requestSuggestions(value);
};
requestSuggestions = _.debounce((value) => {
const payload = {
value,
series: this.props.series
};
return acc;
}, []);
} else {
const fuse = new Fuse(series, fuseOptions);
suggestions = fuse.search(value);
}
workerInstance.postMessage(payload);
}, 250);
onSuggestionsReceived = (message) => {
this.setState({
suggestions: message.data
});
this.setState({ suggestions });
}
onSuggestionsClearRequested = () => {

View File

@@ -1,63 +0,0 @@
import Fuse from 'fuse.js';
const fuseOptions = {
shouldSort: true,
includeMatches: true,
threshold: 0.3,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
'title',
'alternateTitles.title',
'tags.label'
]
};
function getSuggestions(series, value) {
const limit = 10;
let suggestions = [];
if (value.length === 1) {
for (let i = 0; i < series.length; i++) {
const s = series[i];
if (s.firstCharacter === value.toLowerCase()) {
suggestions.push({
item: series[i],
indices: [
[0, 0]
],
matches: [
{
value: s.title,
key: 'title'
}
],
arrayIndex: 0
});
if (suggestions.length > limit) {
break;
}
}
}
} else {
const fuse = new Fuse(series, fuseOptions);
suggestions = fuse.search(value, { limit });
}
return suggestions;
}
self.addEventListener('message', (e) => {
if (!e) {
return;
}
const {
series,
value
} = e.data;
self.postMessage(getSuggestions(series, value));
});

View File

@@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import { isLocked } from 'Utilities/scrollLock';
import { scrollDirections } from 'Helpers/Props';
import OverlayScroller from 'Components/Scroller/OverlayScroller';
@@ -9,15 +8,6 @@ import styles from './PageContentBody.css';
class PageContentBody extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._isMobile = isMobileUtil();
}
//
// Listeners
@@ -36,12 +26,13 @@ class PageContentBody extends Component {
const {
className,
innerClassName,
isSmallScreen,
children,
dispatch,
...otherProps
} = this.props;
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
const ScrollerComponent = isSmallScreen ? Scroller : OverlayScroller;
return (
<ScrollerComponent
@@ -61,6 +52,7 @@ class PageContentBody extends Component {
PageContentBody.propTypes = {
className: PropTypes.string,
innerClassName: PropTypes.string,
isSmallScreen: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onScroll: PropTypes.func,
dispatch: PropTypes.func

View File

@@ -0,0 +1,17 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import PageContentBody from './PageContentBody';
function createMapStateToProps() {
return createSelector(
createDimensionsSelector(),
(dimensions) => {
return {
isSmallScreen: dimensions.isSmallScreen
};
}
);
}
export default connect(createMapStateToProps, null, null, { forwardRef: true })(PageContentBody);

View File

@@ -1,17 +1,17 @@
import React from 'react';
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
import PageContentBody from './PageContentBody';
import PageContentBodyConnector from './PageContentBodyConnector';
import styles from './PageContentError.css';
function PageContentError(props) {
return (
<div className={styles.content}>
<PageContentBody>
<PageContentBodyConnector>
<ErrorBoundaryError
{...props}
message='There was an error loading this page'
/>
</PageContentBody>
</PageContentBodyConnector>
</div>
);
}

View File

@@ -1,3 +1,4 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import dimensions from 'Styles/Variables/dimensions';
@@ -17,7 +18,7 @@ class PageJumpBar extends Component {
this.state = {
height: 0,
visibleItems: props.items.order
visibleItems: props.items
};
}
@@ -51,47 +52,29 @@ class PageJumpBar extends Component {
minimumItems
} = this.props;
if (!items) {
return;
}
const {
characters,
order
} = items;
const height = this.state.height;
const maximumItems = Math.floor(height / ITEM_HEIGHT);
const diff = order.length - maximumItems;
const diff = items.length - maximumItems;
if (diff < 0) {
this.setState({ visibleItems: order });
this.setState({ visibleItems: items });
return;
}
if (order.length < minimumItems) {
this.setState({ visibleItems: order });
if (items.length < minimumItems) {
this.setState({ visibleItems: items });
return;
}
// get first, last, and most common in between to make up numbers
const visibleItems = [order[0]];
const removeDiff = Math.ceil(items.length / maximumItems);
const sorted = order.slice(1, -1).map((x) => characters[x]).sort((a, b) => b - a);
const minCount = sorted[maximumItems - 3];
const greater = sorted.reduce((acc, value) => acc + (value > minCount ? 1 : 0), 0);
let minAllowed = maximumItems - 2 - greater;
for (let i = 1; i < order.length - 1; i++) {
if (characters[order[i]] > minCount) {
visibleItems.push(order[i]);
} else if (characters[order[i]] === minCount && minAllowed > 0) {
visibleItems.push(order[i]);
minAllowed--;
const visibleItems = _.reduce(items, (acc, item, index) => {
if (index % removeDiff === 0) {
acc.push(item);
}
}
visibleItems.push(order[order.length - 1]);
return acc;
}, []);
this.setState({ visibleItems });
}
@@ -146,7 +129,7 @@ class PageJumpBar extends Component {
}
PageJumpBar.propTypes = {
items: PropTypes.object.isRequired,
items: PropTypes.arrayOf(PropTypes.string).isRequired,
minimumItems: PropTypes.number.isRequired,
onItemPress: PropTypes.func.isRequired
};

View File

@@ -1,5 +1,5 @@
.jumpBarItem {
flex: 1 1 $jumpBarItemHeight;
flex: 1 0 $jumpBarItemHeight;
border-bottom: 1px solid $borderColor;
text-align: center;
font-weight: bold;

View File

@@ -165,23 +165,6 @@ const links = [
to: '/system/logs/files'
}
]
},
{
iconName: icons.DEBUG,
hidden: true,
title: 'Diagnostics',
to: '/diag/status',
children: [
{
title: 'Status',
to: '/diag/status'
},
{
title: 'Script Console',
to: '/diag/script'
}
]
}
];
@@ -490,10 +473,6 @@ class PageSidebar extends Component {
const isActiveParent = activeParent === link.to;
const hasActiveChild = hasActiveChildLink(link, pathname);
if (link.hidden && !isActiveParent && !hasActiveChild) {
return null;
}
return (
<PageSidebarItem
key={link.to}

View File

@@ -41,7 +41,6 @@ function PageToolbarButton(props) {
}
PageToolbarButton.propTypes = {
...Link.propTypes,
label: PropTypes.string.isRequired,
iconName: PropTypes.object.isRequired,
spinningName: PropTypes.object,

View File

@@ -1,6 +1,6 @@
.sectionContainer {
display: flex;
flex: 1 1 auto;
flex: 1 1 10%;
overflow: hidden;
}

View File

@@ -15,6 +15,7 @@ import styles from './PageToolbarSection.css';
const BUTTON_WIDTH = parseInt(dimensions.toolbarButtonWidth);
const SEPARATOR_MARGIN = parseInt(dimensions.toolbarSeparatorMargin);
const SEPARATOR_WIDTH = 2 * SEPARATOR_MARGIN + 1;
const SEPARATOR_NAME = 'PageToolbarSeparator';
function calculateOverflowItems(children, isMeasured, width, collapseButtons) {
let buttonCount = 0;
@@ -22,7 +23,9 @@ function calculateOverflowItems(children, isMeasured, width, collapseButtons) {
const validChildren = [];
forEach(children, (child) => {
if (Object.keys(child.props).length === 0) {
const name = child.type.name;
if (name === SEPARATOR_NAME) {
separatorCount++;
} else {
buttonCount++;
@@ -65,14 +68,12 @@ function calculateOverflowItems(children, isMeasured, width, collapseButtons) {
}
validChildren.forEach((child, index) => {
const isSeparator = Object.keys(child.props).length === 0;
if (actualButtons < maxButtons) {
if (!isSeparator) {
if (child.type.name !== SEPARATOR_NAME) {
buttons.push(child);
actualButtons++;
}
} else if (!isSeparator) {
} else if (child.type.name !== SEPARATOR_NAME) {
overflowItems.push(child.props);
}
});

View File

@@ -2,10 +2,6 @@
/* Placeholder */
}
.track {
/* Placeholder */
}
.thumb {
min-height: 100px;
border: 1px solid transparent;

View File

@@ -37,10 +37,6 @@ class OverlayScroller extends Component {
_setScrollRef = (ref) => {
this._scroller = ref;
if (ref) {
this.props.registerScroller(ref.view);
}
}
_renderThumb = (props) => {
@@ -64,7 +60,6 @@ class OverlayScroller extends Component {
return (
<div
className={styles.track}
style={finalStyle}
{...props}
/>
@@ -83,7 +78,6 @@ class OverlayScroller extends Component {
return (
<div
className={styles.track}
style={finalStyle}
{...props}
/>
@@ -163,8 +157,7 @@ OverlayScroller.propTypes = {
autoHide: PropTypes.bool.isRequired,
autoScroll: PropTypes.bool.isRequired,
children: PropTypes.node,
onScroll: PropTypes.func,
registerScroller: PropTypes.func
onScroll: PropTypes.func
};
OverlayScroller.defaultProps = {
@@ -172,8 +165,7 @@ OverlayScroller.defaultProps = {
trackClassName: styles.thumb,
scrollDirection: scrollDirections.VERTICAL,
autoHide: false,
autoScroll: true,
registerScroller: () => {}
autoScroll: true
};
export default OverlayScroller;

View File

@@ -17,18 +17,12 @@ class Scroller extends Component {
componentDidMount() {
const {
scrollDirection,
autoFocus,
scrollTop
} = this.props;
if (this.props.scrollTop != null) {
this._scroller.scrollTop = scrollTop;
}
if (autoFocus && scrollDirection !== scrollDirections.NONE) {
this._scroller.focus({ preventScroll: true });
}
}
//
@@ -36,8 +30,6 @@ class Scroller extends Component {
_setScrollerRef = (ref) => {
this._scroller = ref;
this.props.registerScroller(ref);
}
//
@@ -51,7 +43,6 @@ class Scroller extends Component {
children,
scrollTop,
onScroll,
registerScroller,
...otherProps
} = this.props;
@@ -64,7 +55,6 @@ class Scroller extends Component {
styles[scrollDirection],
autoScroll && styles.autoScroll
)}
tabIndex={-1}
{...otherProps}
>
{children}
@@ -77,19 +67,15 @@ class Scroller extends Component {
Scroller.propTypes = {
className: PropTypes.string,
scrollDirection: PropTypes.oneOf(scrollDirections.all).isRequired,
autoFocus: PropTypes.bool.isRequired,
autoScroll: PropTypes.bool.isRequired,
scrollTop: PropTypes.number,
children: PropTypes.node,
onScroll: PropTypes.func,
registerScroller: PropTypes.func
onScroll: PropTypes.func
};
Scroller.defaultProps = {
scrollDirection: scrollDirections.VERTICAL,
autoFocus: true,
autoScroll: true,
registerScroller: () => {}
autoScroll: true
};
export default Scroller;

View File

@@ -1,7 +1,3 @@
.tableContainer {
width: 100%;
}
.tableBodyContainer {
position: relative;
}

View File

@@ -1,10 +1,12 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { WindowScroller } from 'react-virtualized';
import { isLocked } from 'Utilities/scrollLock';
import { scrollDirections } from 'Helpers/Props';
import Measure from 'Components/Measure';
import Scroller from 'Components/Scroller/Scroller';
import { WindowScroller, Grid } from 'react-virtualized';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import VirtualTableBody from './VirtualTableBody';
import styles from './VirtualTable.css';
const ROW_HEIGHT = 38;
@@ -42,37 +44,28 @@ class VirtualTable extends Component {
width: 0
};
this._grid = null;
this._isInitialized = false;
}
componentDidUpdate(prevProps, prevState) {
const {
items,
scrollIndex
} = this.props;
componentDidMount() {
this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody);
}
const {
width
} = this.state;
if (this._grid && (prevState.width !== width || hasDifferentItemsOrOrder(prevProps.items, items))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
componentDidUpdate(prevProps, preState) {
const { scrollIndex, rowHeight } = this.props;
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
this._grid.scrollToCell({
rowIndex: scrollIndex,
columnIndex: 0
});
const scrollTop = (scrollIndex + 1) * rowHeight + 20;
this.props.onScroll({ scrollTop });
}
}
//
// Control
setGridRef = (ref) => {
this._grid = ref;
rowGetter = ({ index }) => {
return this.props.items[index];
}
//
@@ -84,18 +77,36 @@ class VirtualTable extends Component {
});
}
onSectionRendered = () => {
if (!this._isInitialized && this._contentBodyNode) {
this.props.onRender();
this._isInitialized = true;
}
}
onScroll = (props) => {
if (isLocked()) {
return;
}
const { onScroll } = this.props;
onScroll(props);
}
//
// Render
render() {
const {
isSmallScreen,
className,
items,
scroller,
isSmallScreen,
header,
headerHeight,
scrollTop,
rowRenderer,
onScroll,
...otherProps
} = this.props;
@@ -103,89 +114,66 @@ class VirtualTable extends Component {
width
} = this.state;
const gridStyle = {
boxSizing: undefined,
direction: undefined,
height: undefined,
position: undefined,
willChange: undefined,
overflow: undefined,
width: undefined
};
const containerStyle = {
position: undefined
};
return (
<WindowScroller
scrollElement={isSmallScreen ? undefined : scroller}
>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return null;
}
return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<Measure onMeasure={this.onMeasure}>
<WindowScroller
scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
onScroll={this.onScroll}
>
{({ height, isScrolling }) => {
return (
<Scroller
className={className}
scrollDirection={scrollDirections.HORIZONTAL}
>
{header}
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
autoContainerWidth={true}
autoHeight={true}
autoWidth={true}
width={width}
height={height}
headerHeight={height - headerHeight}
rowHeight={ROW_HEIGHT}
rowCount={items.length}
columnCount={1}
columnWidth={width}
scrollTop={scrollTop}
onScroll={onChildScroll}
overscanRowCount={2}
cellRenderer={rowRenderer}
overscanIndicesGetter={overscanIndicesGetter}
scrollToAlignment={'start'}
isScrollingOptout={true}
className={styles.tableBodyContainer}
style={gridStyle}
containerStyle={containerStyle}
{...otherProps}
/>
</div>
<VirtualTableBody
autoContainerWidth={true}
width={width}
height={height}
headerHeight={height - headerHeight}
rowHeight={ROW_HEIGHT}
rowCount={items.length}
columnCount={1}
scrollTop={scrollTop}
autoHeight={true}
overscanRowCount={2}
cellRenderer={rowRenderer}
columnWidth={width}
overscanIndicesGetter={overscanIndicesGetter}
onSectionRendered={this.onSectionRendered}
{...otherProps}
/>
</Scroller>
</Measure>
);
}
}
</WindowScroller>
);
}
}
</WindowScroller>
</Measure>
);
}
}
VirtualTable.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
className: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
scrollTop: PropTypes.number.isRequired,
scrollIndex: PropTypes.number,
scroller: PropTypes.instanceOf(Element).isRequired,
contentBody: PropTypes.object.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
header: PropTypes.node.isRequired,
headerHeight: PropTypes.number.isRequired,
rowRenderer: PropTypes.func.isRequired,
rowHeight: PropTypes.number.isRequired
rowHeight: PropTypes.number.isRequired,
onRender: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired
};
VirtualTable.defaultProps = {
className: styles.tableContainer,
headerHeight: 38
headerHeight: 38,
onRender: () => {}
};
export default VirtualTable;

View File

@@ -0,0 +1,3 @@
.tableBodyContainer {
position: relative;
}

View File

@@ -0,0 +1,40 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Grid } from 'react-virtualized';
import styles from './VirtualTableBody.css';
class VirtualTableBody extends Component {
//
// Render
render() {
return (
<Grid
{...this.props}
style={{
boxSizing: undefined,
direction: undefined,
height: undefined,
position: undefined,
willChange: undefined,
overflow: undefined,
width: undefined
}}
containerStyle={{
position: undefined
}}
/>
);
}
}
VirtualTableBody.propTypes = {
className: PropTypes.string.isRequired
};
VirtualTableBody.defaultProps = {
className: styles.tableBodyContainer
};
export default VirtualTableBody;

View File

@@ -8,16 +8,6 @@ export const shortcuts = {
name: 'Open This Modal'
},
CLOSE_MODAL: {
key: 'Esc',
name: 'Close Current Modal'
},
ACCEPT_CONFIRM_MODAL: {
key: 'Enter',
name: 'Accept Confirmation Modal'
},
SERIES_SEARCH_INPUT: {
key: 's',
name: 'Focus Search Box'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,44 +0,0 @@
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router-dom';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import Switch from 'Components/Router/Switch';
import StatusConnector from './Status/StatusConnector';
import ScriptConnector from './Script/ScriptConnector';
class Diagnostic extends Component {
//
// Render
render() {
return (
<Switch>
<Route
exact={true}
path="/diag/status"
component={StatusConnector}
/>
<Route
exact={true}
path="/diag/script"
component={ScriptConnector}
/>
{/* Redirect root to status */}
<Route
exact={true}
path="/diag"
render={() => {
return (
<Redirect
to={getPathWithUrlBase('/diag/status')}
/>
);
}}
/>
</Switch>
);
}
}
export default Diagnostic;

View File

@@ -1,82 +0,0 @@
import ReactMonacoEditor from 'react-monaco-editor';
import shallowEqual from 'shallowequal';
// All editor features -> 7.56 MiB
// import 'monaco-editor/esm/vs/editor/editor.all';
// Only the needed editor features -> 6.88 MiB
import 'monaco-editor/esm/vs/editor/browser/controller/coreCommands';
import 'monaco-editor/esm/vs/editor/browser/widget/codeEditorWidget';
// import 'monaco-editor/esm/vs/editor/browser/widget/diffEditorWidget';
// import 'monaco-editor/esm/vs/editor/browser/widget/diffNavigator';
import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching';
import 'monaco-editor/esm/vs/editor/contrib/caretOperations/caretOperations';
import 'monaco-editor/esm/vs/editor/contrib/caretOperations/transpose';
// import 'monaco-editor/esm/vs/editor/contrib/clipboard/clipboard';
// import 'monaco-editor/esm/vs/editor/contrib/codeAction/codeActionContributions';
// import 'monaco-editor/esm/vs/editor/contrib/codelens/codelensController';
// import 'monaco-editor/esm/vs/editor/contrib/colorPicker/colorDetector';
import 'monaco-editor/esm/vs/editor/contrib/comment/comment';
import 'monaco-editor/esm/vs/editor/contrib/contextmenu/contextmenu';
import 'monaco-editor/esm/vs/editor/contrib/cursorUndo/cursorUndo';
import 'monaco-editor/esm/vs/editor/contrib/dnd/dnd';
import 'monaco-editor/esm/vs/editor/contrib/find/findController';
import 'monaco-editor/esm/vs/editor/contrib/folding/folding';
import 'monaco-editor/esm/vs/editor/contrib/fontZoom/fontZoom';
// import 'monaco-editor/esm/vs/editor/contrib/format/formatActions';
// import 'monaco-editor/esm/vs/editor/contrib/goToDefinition/goToDefinitionCommands';
// import 'monaco-editor/esm/vs/editor/contrib/goToDefinition/goToDefinitionMouse';
// import 'monaco-editor/esm/vs/editor/contrib/gotoError/gotoError';
import 'monaco-editor/esm/vs/editor/contrib/hover/hover';
import 'monaco-editor/esm/vs/editor/contrib/inPlaceReplace/inPlaceReplace';
import 'monaco-editor/esm/vs/editor/contrib/linesOperations/linesOperations';
// import 'monaco-editor/esm/vs/editor/contrib/links/links';
import 'monaco-editor/esm/vs/editor/contrib/multicursor/multicursor';
// import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints';
// import 'monaco-editor/esm/vs/editor/contrib/referenceSearch/referenceSearch';
import 'monaco-editor/esm/vs/editor/contrib/rename/rename';
import 'monaco-editor/esm/vs/editor/contrib/smartSelect/smartSelect';
// import 'monaco-editor/esm/vs/editor/contrib/snippet/snippetController2';
// import 'monaco-editor/esm/vs/editor/contrib/suggest/suggestController';
// import 'monaco-editor/esm/vs/editor/contrib/tokenization/tokenization';
// import 'monaco-editor/esm/vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode';
import 'monaco-editor/esm/vs/editor/contrib/wordHighlighter/wordHighlighter';
import 'monaco-editor/esm/vs/editor/contrib/wordOperations/wordOperations';
import 'monaco-editor/esm/vs/editor/contrib/wordPartOperations/wordPartOperations';
// csharp&json language
import 'monaco-editor/esm/vs/basic-languages/csharp/csharp';
import 'monaco-editor/esm/vs/basic-languages/csharp/csharp.contribution';
import 'monaco-editor/esm/vs/language/json/monaco.contribution';
import 'monaco-editor/esm/vs/language/json/jsonWorker';
import 'monaco-editor/esm/vs/language/json/jsonMode';
// Create a WebWorker from a blob rather than an url
import * as EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker';
import * as JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker';
self.MonacoEnvironment = {
getWorker: (moduleId, label) => {
if (label === 'editorWorkerService') {
return new EditorWorker();
}
if (label === 'json') {
return new JsonWorker();
}
return null;
}
};
class MonacoEditor extends ReactMonacoEditor {
// ReactMonacoEditor should've been PureComponent
shouldComponentUpdate(nextProps, nextState) {
if (!shallowEqual(this.props, nextProps)) {
return true;
}
return false;
}
}
export default MonacoEditor;

View File

@@ -1,93 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchStatus, updateScript, validateScript, executeScript } from 'Store/Actions/diagnosticActions';
import ScriptConsole from './ScriptConsole';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Alert from 'Components/Alert';
import { kinds } from 'Helpers/Props';
import PageContent from 'Components/Page/PageContent';
function createMapStateToProps() {
return createSelector(
(state) => state.diagnostic,
(diag) => {
return {
isStatusPopulated: diag.status.isPopulated,
isScriptConsoleEnabled: diag.status.item.scriptConsoleEnabled,
isExecuting: diag.script.isExecuting || false,
isDebugging: diag.script.isDebugging || false,
isValidating: diag.script.isValidating,
code: diag.script.code,
result: diag.script.result,
validation: diag.script.validation,
error: diag.script.error
};
}
);
}
const mapDispatchToProps = {
fetchStatus,
updateScript,
validateScript,
executeScript
};
class ScriptConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isStatusPopulated) {
this.props.fetchStatus();
}
}
//
// Render
render() {
if (!this.props.isStatusPopulated) {
return (
<PageContent>
<LoadingIndicator />
</PageContent>
);
} else if (!this.props.isScriptConsoleEnabled) {
return (
<PageContent>
<Alert kind={kinds.WARNING}>
Diagnostic Scripting is disabled
</Alert>
</PageContent>
);
}
return (
<ScriptConsole
{...this.props}
/>
);
}
}
ScriptConnector.propTypes = {
isStatusPopulated: PropTypes.bool.isRequired,
isScriptConsoleEnabled: PropTypes.bool,
isExecuting: PropTypes.bool.isRequired,
isDebugging: PropTypes.bool.isRequired,
isValidating: PropTypes.bool.isRequired,
code: PropTypes.string,
result: PropTypes.object,
error: PropTypes.object,
validation: PropTypes.object,
fetchStatus: PropTypes.func.isRequired,
updateScript: PropTypes.func.isRequired,
validateScript: PropTypes.func.isRequired,
executeScript: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ScriptConnector);

View File

@@ -1,6 +0,0 @@
.split {
display: flex;
justify-content: space-between;
overflow: hidden;
height: 100%;
}

View File

@@ -1,139 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component, lazy, Suspense } from 'react';
import { icons } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageContent from 'Components/Page/PageContent';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import styles from './ScriptConsole.css';
// Lazy load the Monaco Editor since it's a big bundle
const MonacoEditor = lazy(() => import(/* webpackChunkName: "monaco-editor" */ './MonacoEditor'));
const DefaultOptions = {
selectOnLineNumbers: true,
scrollBeyondLastLine: false
};
const DefaultResultOptions = {
...DefaultOptions,
readOnly: true
};
class ScriptConsole extends Component {
//
// Lifecycle
editorDidMount = (editor, monaco) => {
console.log('editorDidMount', editor);
editor.focus();
this.monaco = monaco;
this.editor = editor;
this.updateValidation(this.props.validation);
}
updateValidation(validation) {
if (!this.monaco) {
return;
}
let diagnostics = [];
if (validation && validation.errorDiagnostics) {
diagnostics = validation.errorDiagnostics;
}
const model = this.editor.getModel();
this.monaco.editor.setModelMarkers(model, 'editor', diagnostics);
}
onChange = (newValue, e) => {
this.props.updateScript({ code: newValue });
this.validateCode();
}
validateCode = _.debounce(() => {
const code = this.props.code;
this.props.validateScript({ code });
}, 250, { leading: false, trailing: true })
onExecuteScriptPress = () => {
const code = this.props.code;
this.props.executeScript({ code });
}
onDebugScriptPress = () => {
const code = this.props.code;
this.props.executeScript({ code, debug: true });
}
//
// Render
render() {
const code = this.props.code;
const result = JSON.stringify(this.props.result, null, 2);
this.updateValidation(this.props.validation);
return (
<PageContent>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Run"
iconName={this.props.isExecuting ? icons.REFRESH : icons.SCRIPT_RUN}
isSpinning={this.props.isExecuting}
onPress={this.onExecuteScriptPress}
/>
<PageToolbarButton
label="Debug"
iconName={this.props.isDebugging ? icons.REFRESH : icons.SCRIPT_DEBUG}
isSpinning={this.props.isDebugging}
onPress={this.onDebugScriptPress}
/>
</PageToolbarSection>
</PageToolbar>
<Suspense fallback={<LoadingIndicator />}>
<div className={styles.split}>
<MonacoEditor
language="csharp"
theme="vs-light"
width="50%"
value={code}
options={DefaultOptions}
onChange={this.onChange}
editorDidMount={this.editorDidMount}
/>
<MonacoEditor
language="json"
theme="vs-light"
width="50%"
value={result}
options={DefaultResultOptions}
/>
</div>
</Suspense>
</PageContent>
);
}
}
ScriptConsole.propTypes = {
isExecuting: PropTypes.bool.isRequired,
isDebugging: PropTypes.bool.isRequired,
isValidating: PropTypes.bool.isRequired,
code: PropTypes.string,
result: PropTypes.object,
error: PropTypes.object,
validation: PropTypes.object,
updateScript: PropTypes.func.isRequired,
validateScript: PropTypes.func.isRequired,
executeScript: PropTypes.func.isRequired
};
export default ScriptConsole;

View File

@@ -1,5 +0,0 @@
.descriptionList {
composes: descriptionList from '~Components/DescriptionList/DescriptionList.css';
margin-bottom: 10px;
}

View File

@@ -1,93 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FieldSet from 'Components/FieldSet';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import styles from './Statistics.css';
import formatBytes from 'Utilities/Number/formatBytes';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import moment from 'moment';
function formatValue(val, formatter) {
if (val === undefined) {
return 'n/a';
}
if (formatter) {
return formatter(val);
}
return val;
}
class Statistics extends Component {
//
// Render
render() {
const {
process,
databaseMain,
databaseLog,
commandsExecuted
} = this.props;
return (
<FieldSet legend="Statistics">
<DescriptionList className={styles.descriptionList}>
<DescriptionListItem
title="Up Time"
data={formatValue(process.startTime, (startTime) => formatTimeSpan(moment().diff(startTime)))}
/>
<DescriptionListItem
title="Processor Time"
data={formatValue(process.totalProcessorTime, formatTimeSpan)}
/>
<DescriptionListItem
title="Memory Working Set"
data={formatValue(process.workingSet, formatBytes)}
/>
<DescriptionListItem
title="Memory Virtual Size"
data={formatValue(process.virtualMemorySize, formatBytes)}
/>
<DescriptionListItem
title="Main Database Size"
data={formatValue(databaseMain.size, formatBytes)}
/>
<DescriptionListItem
title="Logs Database Size"
data={formatValue(databaseLog.size, formatBytes)}
/>
<DescriptionListItem
title="Commands Executed"
data={formatValue(commandsExecuted, formatBytes)}
/>
</DescriptionList>
</FieldSet>
);
}
}
Statistics.propTypes = {
process: PropTypes.object,
databaseMain: PropTypes.object,
databaseLog: PropTypes.object,
commandsExecuted: PropTypes.number
};
Statistics.defaultProps = {
process: {},
databaseMain: {},
databaseLog: {}
};
export default Statistics;

View File

@@ -1,46 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons } from 'Helpers/Props';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import Statistics from './Statistics/Statistics';
class Status extends Component {
//
// Render
render() {
return (
<PageContent title="Diagnostic Status">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Refresh"
iconName={icons.REFRESH}
isSpinning={this.props.isStatusFetching}
onPress={this.props.onRefreshPress}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
<Statistics
{...this.props.status}
/>
</PageContentBody>
</PageContent>
);
}
}
Status.propTypes = {
status: PropTypes.object.isRequired,
isStatusFetching: PropTypes.bool.isRequired,
onRefreshPress: PropTypes.func.isRequired
};
export default Status;

View File

@@ -1,59 +0,0 @@
// @ts-check
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchStatus } from 'Store/Actions/diagnosticActions';
import Status from './Status';
function createMapStateToProps() {
return createSelector(
(state) => state.diagnostic.status,
(status) => {
return {
isStatusFetching: status.isFetching,
status: status.item
};
}
);
}
const mapDispatchToProps = {
fetchStatus
};
class DiagnosticConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchStatus();
}
//
// Listeners
onRefreshPress = () => {
this.props.fetchStatus();
}
//
// Render
render() {
return (
<Status
onRefreshPress={this.onRefreshPress}
{...this.props}
/>
);
}
}
DiagnosticConnector.propTypes = {
status: PropTypes.object.isRequired,
fetchStatus: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(DiagnosticConnector);

View File

@@ -35,10 +35,6 @@ function EpisodeQuality(props) {
isCutoffNotMet
} = props;
if (!quality) {
return null;
}
return (
<Label
className={className}

View File

@@ -27,7 +27,7 @@ function EpisodeStatus(props) {
size
} = queueItem;
const progress = size ? (100 - sizeleft / size * 100) : 0;
const progress = (100 - sizeleft / size * 100);
return (
<div className={styles.center}>

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import padNumber from 'Utilities/Number/padNumber';
import styles from './SceneInfo.css';
function SceneInfo(props) {
@@ -61,10 +60,6 @@ function SceneInfo(props) {
key={alternateTitle.title}
>
{alternateTitle.title}
{
alternateTitle.sceneSeasonNumber !== -1 &&
<span> (S{padNumber(alternateTitle.sceneSeasonNumber, 2)})</span>
}
</div>
);
})

View File

@@ -81,7 +81,6 @@ import {
faSignOutAlt as fasSignOutAlt,
faSitemap as fasSitemap,
faSpinner as fasSpinner,
faStepForward as fasStepForward,
faSort as fasSort,
faSortDown as fasSortDown,
faSortUp as fasSortUp,
@@ -127,7 +126,6 @@ export const CLONE = farClone;
export const COLLAPSE = fasChevronCircleUp;
export const COMPUTER = fasDesktop;
export const DANGER = fasExclamationCircle;
export const DEBUG = fasBug;
export const DELETE = fasTrashAlt;
export const DOWNLOAD = fasDownload;
export const DOWNLOADED = fasDownload;
@@ -182,8 +180,6 @@ export const REORDER = fasBars;
export const RSS = fasRss;
export const SAVE = fasSave;
export const SCHEDULED = farClock;
export const SCRIPT_DEBUG = fasStepForward;
export const SCRIPT_RUN = fasPlay;
export const SCORE = fasUserPlus;
export const SEARCH = fasSearch;
export const SERIES_CONTINUING = fasPlay;

View File

@@ -66,7 +66,6 @@ const columns = [
{
name: 'size',
label: 'Size',
isSortable: true,
isVisible: true
},
{

View File

@@ -47,7 +47,6 @@ class InteractiveImportModal extends Component {
return (
<Modal
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
{

View File

@@ -62,10 +62,7 @@ class SelectSeriesModalContent extends Component {
onChange={this.onFilterChange}
/>
<Scroller
className={styles.scroller}
autoFocus={false}
>
<Scroller className={styles.scroller}>
{
items.map((item) => {
return item.title.toLowerCase().includes(filter) ?

View File

@@ -11,7 +11,7 @@ function createMapStateToProps() {
createAllSeriesSelector(),
(items) => {
return {
items: [...items].sort((a, b) => {
items: items.sort((a, b) => {
if (a.sortTitle < b.sortTitle) {
return -1;
}

View File

@@ -6,7 +6,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
import { align, sortDirections } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import FilterMenu from 'Components/Menu/FilterMenu';
@@ -137,7 +137,7 @@ class SeasonPass extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
<PageContentBodyConnector>
{
isFetching && !isPopulated &&
<LoadingIndicator />
@@ -183,7 +183,7 @@ class SeasonPass extends Component {
!error && isPopulated && !items.length &&
<NoSeries totalItems={totalItems} />
}
</PageContentBody>
</PageContentBodyConnector>
<SeasonPassFooter
selectedCount={this.getSelectedIds().length}

View File

@@ -13,7 +13,7 @@
.episodeNumber {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 65px;
width: 50px;
}
.size {

View File

@@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React from 'react';
import formatBytes from 'Utilities/Number/formatBytes';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import styles from './SeasonInfo.css';
@@ -9,8 +8,7 @@ function SeasonInfo(props) {
const {
totalEpisodeCount,
monitoredEpisodeCount,
episodeFileCount,
sizeOnDisk
episodeFileCount
} = props;
return (
@@ -35,13 +33,6 @@ function SeasonInfo(props) {
title="With Files"
data={episodeFileCount}
/>
<DescriptionListItem
titleClassName={styles.title}
descriptionClassName={styles.description}
title="Size on Disk"
data={formatBytes(sizeOnDisk)}
/>
</DescriptionList>
);
}
@@ -49,8 +40,7 @@ function SeasonInfo(props) {
SeasonInfo.propTypes = {
totalEpisodeCount: PropTypes.number.isRequired,
monitoredEpisodeCount: PropTypes.number.isRequired,
episodeFileCount: PropTypes.number.isRequired,
sizeOnDisk: PropTypes.number.isRequired
episodeFileCount: PropTypes.number.isRequired
};
export default SeasonInfo;

View File

@@ -15,7 +15,7 @@ import Measure from 'Components/Measure';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
@@ -310,7 +310,7 @@ class SeriesDetails extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBody innerClassName={styles.innerContentBody}>
<PageContentBodyConnector innerClassName={styles.innerContentBody}>
<div className={styles.header}>
<div
className={styles.backdrop}
@@ -646,7 +646,7 @@ class SeriesDetails extends Component {
showImportMode={false}
onModalClose={this.onInteractiveImportModalClose}
/>
</PageContentBody>
</PageContentBodyConnector>
</PageContent>
);
}

View File

@@ -26,12 +26,6 @@
display: flex;
}
.sizeOnDisk {
margin-left: 10px;
color: #777;
font-size: $defaultFontSize;
}
.expandButton {
composes: link from '~Components/Link/Link.css';
@@ -43,7 +37,7 @@
.left {
display: flex;
align-items: center;
flex: 0 1 350px;
flex: 0 1 300px;
}
.left,
@@ -121,8 +115,4 @@
position: static;
margin: 0;
}
.sizeOnDisk {
display: none;
}
}

View File

@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import isAfter from 'Utilities/Date/isAfter';
import isBefore from 'Utilities/Date/isBefore';
import formatBytes from 'Utilities/Number/formatBytes';
import getToggledRange from 'Utilities/Table/getToggledRange';
import { align, icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import Icon from 'Components/Icon';
@@ -34,7 +33,6 @@ function getSeasonStatistics(episodes) {
let totalEpisodeCount = 0;
let monitoredEpisodeCount = 0;
let hasMonitoredEpisodes = false;
const sizeOnDisk = 0;
episodes.forEach((episode) => {
if (episode.episodeFileId || (episode.monitored && isBefore(episode.airDateUtc))) {
@@ -58,8 +56,7 @@ function getSeasonStatistics(episodes) {
episodeFileCount,
totalEpisodeCount,
monitoredEpisodeCount,
hasMonitoredEpisodes,
sizeOnDisk
hasMonitoredEpisodes
};
}
@@ -208,7 +205,6 @@ class SeriesDetailsSeason extends Component {
seasonNumber,
items,
columns,
statistics,
isSaving,
isExpanded,
isSearching,
@@ -275,20 +271,11 @@ class SeriesDetailsSeason extends Component {
totalEpisodeCount={totalEpisodeCount}
monitoredEpisodeCount={monitoredEpisodeCount}
episodeFileCount={episodeFileCount}
sizeOnDisk={statistics.sizeOnDisk}
/>
</div>
}
position={tooltipPositions.BOTTOM}
/>
{
statistics.sizeOnDisk ?
<div className={styles.sizeOnDisk}>
{formatBytes(statistics.sizeOnDisk)}
</div> :
null
}
</div>
<Link
@@ -517,7 +504,6 @@ SeriesDetailsSeason.propTypes = {
seasonNumber: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
statistics: PropTypes.object.isRequired,
isSaving: PropTypes.bool,
isExpanded: PropTypes.bool,
isSearching: PropTypes.bool.isRequired,
@@ -530,8 +516,4 @@ SeriesDetailsSeason.propTypes = {
onSearchPress: PropTypes.func.isRequired
};
SeriesDetailsSeason.defaultProps = {
statistics: {}
};
export default SeriesDetailsSeason;

View File

@@ -1,3 +1,4 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -26,8 +27,8 @@ function createMapStateToProps() {
seasonNumber
}));
const episodesInSeason = episodes.items.filter((episode) => episode.seasonNumber === seasonNumber);
const sortedEpisodes = episodesInSeason.sort((a, b) => b.episodeNumber - a.episodeNumber);
const episodesInSeason = _.filter(episodes.items, { seasonNumber });
const sortedEpisodes = _.orderBy(episodesInSeason, 'episodeNumber', 'desc');
return {
items: sortedEpisodes,

View File

@@ -3,16 +3,13 @@ import React, { Component } from 'react';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { align, icons, sortDirections } from 'Helpers/Props';
import { align, sortDirections } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import FilterMenu from 'Components/Menu/FilterMenu';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import NoSeries from 'Series/NoSeries';
@@ -21,6 +18,58 @@ import SeriesEditorRowConnector from './SeriesEditorRowConnector';
import SeriesEditorFooter from './SeriesEditorFooter';
import SeriesEditorFilterModalConnector from './SeriesEditorFilterModalConnector';
function getColumns(showLanguageProfile) {
return [
{
name: 'status',
isSortable: true,
isVisible: true
},
{
name: 'sortTitle',
label: 'Title',
isSortable: true,
isVisible: true
},
{
name: 'qualityProfileId',
label: 'Quality Profile',
isSortable: true,
isVisible: true
},
{
name: 'languageProfileId',
label: 'Language Profile',
isSortable: true,
isVisible: showLanguageProfile
},
{
name: 'seriesType',
label: 'Series Type',
isSortable: false,
isVisible: true
},
{
name: 'seasonFolder',
label: 'Season Folder',
isSortable: true,
isVisible: true
},
{
name: 'path',
label: 'Path',
isSortable: true,
isVisible: true
},
{
name: 'tags',
label: 'Tags',
isSortable: false,
isVisible: true
}
];
}
class SeriesEditor extends Component {
//
@@ -34,7 +83,8 @@ class SeriesEditor extends Component {
allUnselected: false,
lastToggled: null,
selectedState: {},
isOrganizingSeriesModalOpen: false
isOrganizingSeriesModalOpen: false,
columns: getColumns(props.showLanguageProfile)
};
}
@@ -102,7 +152,6 @@ class SeriesEditor extends Component {
error,
totalItems,
items,
columns,
selectedFilterKey,
filters,
customFilters,
@@ -113,7 +162,7 @@ class SeriesEditor extends Component {
isDeleting,
deleteError,
isOrganizingSeries,
onTableOptionChange,
showLanguageProfile,
onSortPress,
onFilterSelect
} = this.props;
@@ -121,7 +170,8 @@ class SeriesEditor extends Component {
const {
allSelected,
allUnselected,
selectedState
selectedState,
columns
} = this.state;
const selectedSeriesIds = this.getSelectedIds();
@@ -131,18 +181,6 @@ class SeriesEditor extends Component {
<PageToolbar>
<PageToolbarSection />
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
onTableOptionChange={onTableOptionChange}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<PageToolbarSeparator />
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
@@ -154,7 +192,7 @@ class SeriesEditor extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
<PageContentBodyConnector>
{
isFetching && !isPopulated &&
<LoadingIndicator />
@@ -201,7 +239,7 @@ class SeriesEditor extends Component {
!error && isPopulated && !items.length &&
<NoSeries totalItems={totalItems} />
}
</PageContentBody>
</PageContentBodyConnector>
<SeriesEditorFooter
seriesIds={selectedSeriesIds}
@@ -211,8 +249,7 @@ class SeriesEditor extends Component {
isDeleting={isDeleting}
deleteError={deleteError}
isOrganizingSeries={isOrganizingSeries}
columns={columns}
showLanguageProfile={columns.find((column) => column.name === 'languageProfileId').isVisible}
showLanguageProfile={showLanguageProfile}
onSaveSelected={this.onSaveSelected}
onOrganizeSeriesPress={this.onOrganizeSeriesPress}
/>
@@ -233,7 +270,6 @@ SeriesEditor.propTypes = {
error: PropTypes.object,
totalItems: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
@@ -244,7 +280,7 @@ SeriesEditor.propTypes = {
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
isOrganizingSeries: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired,
showLanguageProfile: PropTypes.bool.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired

View File

@@ -4,7 +4,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { setSeriesEditorSort, setSeriesEditorFilter, setSeriesEditorTableOption, saveSeriesEditor } from 'Store/Actions/seriesEditorActions';
import { setSeriesEditorSort, setSeriesEditorFilter, saveSeriesEditor } from 'Store/Actions/seriesEditorActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
@@ -12,11 +12,13 @@ import SeriesEditor from './SeriesEditor';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.languageProfiles,
createClientSideCollectionSelector('series', 'seriesEditor'),
createCommandExecutingSelector(commandNames.RENAME_SERIES),
(series, isOrganizingSeries) => {
(languageProfiles, series, isOrganizingSeries) => {
return {
isOrganizingSeries,
showLanguageProfile: languageProfiles.items.length > 1,
...series
};
}
@@ -26,7 +28,6 @@ function createMapStateToProps() {
const mapDispatchToProps = {
dispatchSetSeriesEditorSort: setSeriesEditorSort,
dispatchSetSeriesEditorFilter: setSeriesEditorFilter,
dispatchSetSeriesEditorTableOption: setSeriesEditorTableOption,
dispatchSaveSeriesEditor: saveSeriesEditor,
dispatchFetchRootFolders: fetchRootFolders,
dispatchExecuteCommand: executeCommand
@@ -52,10 +53,6 @@ class SeriesEditorConnector extends Component {
this.props.dispatchSetSeriesEditorFilter({ selectedFilterKey });
}
onTableOptionChange = (payload) => {
this.props.dispatchSetSeriesEditorTableOption(payload);
}
onSaveSelected = (payload) => {
this.props.dispatchSaveSeriesEditor(payload);
}
@@ -77,7 +74,6 @@ class SeriesEditorConnector extends Component {
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onSaveSelected={this.onSaveSelected}
onTableOptionChange={this.onTableOptionChange}
/>
);
}
@@ -86,7 +82,6 @@ class SeriesEditorConnector extends Component {
SeriesEditorConnector.propTypes = {
dispatchSetSeriesEditorSort: PropTypes.func.isRequired,
dispatchSetSeriesEditorFilter: PropTypes.func.isRequired,
dispatchSetSeriesEditorTableOption: PropTypes.func.isRequired,
dispatchSaveSeriesEditor: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired

View File

@@ -145,7 +145,7 @@ class SeriesEditorFooter extends Component {
isSaving,
isDeleting,
isOrganizingSeries,
columns,
showLanguageProfile,
onOrganizeSeriesPress
} = this.props;
@@ -192,130 +192,85 @@ class SeriesEditorFooter extends Component {
/>
</div>
<div className={styles.inputContainer}>
<SeriesEditorFooterLabel
label="Quality Profile"
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/>
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
showLanguageProfile &&
<div className={styles.inputContainer}>
<SeriesEditorFooterLabel
label="Language Profile"
isSaving={isSaving && languageProfileId !== NO_CHANGE}
/>
if (!isVisible) {
return null;
}
if (name === 'qualityProfileId') {
return (
<div
key={name}
className={styles.inputContainer}
>
<SeriesEditorFooterLabel
label="Quality Profile"
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/>
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
if (name === 'languageProfileId') {
return (
<div
key={name}
className={styles.inputContainer}
>
<SeriesEditorFooterLabel
label="Language Profile"
isSaving={isSaving && languageProfileId !== NO_CHANGE}
/>
<LanguageProfileSelectInputConnector
name="languageProfileId"
value={languageProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
if (name === 'seriesType') {
return (
<div
key={name}
className={styles.inputContainer}
>
<SeriesEditorFooterLabel
label="Series Type"
isSaving={isSaving && seriesType !== NO_CHANGE}
/>
<SeriesTypeSelectInput
name="seriesType"
value={seriesType}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
if (name === 'seasonFolder') {
return (
<div
key={name}
className={styles.inputContainer}
>
<SeriesEditorFooterLabel
label="Season Folder"
isSaving={isSaving && seasonFolder !== NO_CHANGE}
/>
<SelectInput
name="seasonFolder"
value={seasonFolder}
values={seasonFolderOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
if (name === 'path') {
return (
<div
key={name}
className={styles.inputContainer}
>
<SeriesEditorFooterLabel
label="Root Folder"
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
);
}
})
<LanguageProfileSelectInputConnector
name="languageProfileId"
value={languageProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
}
<div className={styles.inputContainer}>
<SeriesEditorFooterLabel
label="Series Type"
isSaving={isSaving && seriesType !== NO_CHANGE}
/>
<SeriesTypeSelectInput
name="seriesType"
value={seriesType}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<SeriesEditorFooterLabel
label="Season Folder"
isSaving={isSaving && seasonFolder !== NO_CHANGE}
/>
<SelectInput
name="seasonFolder"
value={seasonFolder}
values={seasonFolderOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<SeriesEditorFooterLabel
label="Root Folder"
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<SeriesEditorFooterLabel
@@ -391,7 +346,6 @@ SeriesEditorFooter.propTypes = {
deleteError: PropTypes.object,
isOrganizingSeries: PropTypes.bool.isRequired,
showLanguageProfile: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSaveSelected: PropTypes.func.isRequired,
onOrganizeSeriesPress: PropTypes.func.isRequired
};

View File

@@ -1,7 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import titleCase from 'Utilities/String/titleCase';
import formatBytes from 'Utilities/Number/formatBytes';
import TagListConnector from 'Components/TagListConnector';
import CheckInput from 'Components/Form/CheckInput';
import TableRow from 'Components/Table/TableRow';
@@ -36,7 +36,6 @@ class SeriesEditorRow extends Component {
seriesType,
seasonFolder,
path,
statistics,
tags,
columns,
isSelected,
@@ -51,109 +50,51 @@ class SeriesEditorRow extends Component {
onSelectedChange={onSelectedChange}
/>
<SeriesStatusCell
monitored={monitored}
status={status}
/>
<TableRowCell className={styles.title}>
<SeriesTitleLink
titleSlug={titleSlug}
title={title}
/>
</TableRowCell>
<TableRowCell>
{qualityProfile.name}
</TableRowCell>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'status') {
return (
<SeriesStatusCell
key={name}
monitored={monitored}
status={status}
/>
);
}
if (name === 'sortTitle') {
return (
<TableRowCell
key={name}
className={styles.title}
>
<SeriesTitleLink
titleSlug={titleSlug}
title={title}
/>
</TableRowCell>
);
}
if (name === 'qualityProfileId') {
return (
<TableRowCell key={name}>
{qualityProfile.name}
</TableRowCell>
);
}
if (name === 'languageProfileId') {
return (
<TableRowCell key={name}>
{languageProfile.name}
</TableRowCell>
);
}
if (name === 'seriesType') {
return (
<TableRowCell key={name}>
{titleCase(seriesType)}
</TableRowCell>
);
}
if (name === 'seasonFolder') {
return (
<TableRowCell
key={name}
className={styles.seasonFolder}
>
<CheckInput
name="seasonFolder"
value={seasonFolder}
isDisabled={true}
onChange={this.onSeasonFolderChange}
/>
</TableRowCell>
);
}
if (name === 'path') {
return (
<TableRowCell key={name}>
{path}
</TableRowCell>
);
}
if (name === 'sizeOnDisk') {
return (
<TableRowCell key={name}>
{formatBytes(statistics.sizeOnDisk)}
</TableRowCell>
);
}
if (name === 'tags') {
return (
<TableRowCell key={name}>
<TagListConnector
tags={tags}
/>
</TableRowCell>
);
}
})
_.find(columns, { name: 'languageProfileId' }).isVisible &&
<TableRowCell>
{languageProfile.name}
</TableRowCell>
}
<TableRowCell>
{titleCase(seriesType)}
</TableRowCell>
<TableRowCell className={styles.seasonFolder}>
<CheckInput
name="seasonFolder"
value={seasonFolder}
isDisabled={true}
onChange={this.onSeasonFolderChange}
/>
</TableRowCell>
<TableRowCell>
{path}
</TableRowCell>
<TableRowCell>
<TagListConnector
tags={tags}
/>
</TableRowCell>
</TableRow>
);
}
@@ -170,7 +111,6 @@ SeriesEditorRow.propTypes = {
seriesType: PropTypes.string.isRequired,
seasonFolder: PropTypes.bool.isRequired,
path: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isSelected: PropTypes.bool,

View File

@@ -1,5 +1,13 @@
$hoverScale: 1.05;
.container {
&:hover {
.content {
background-color: $tableRowHoverBackgroundColor;
}
}
}
.content {
display: flex;
flex-grow: 1;

View File

@@ -70,6 +70,7 @@ class SeriesIndexOverview extends Component {
render() {
const {
style,
id,
title,
overview,
@@ -120,7 +121,7 @@ class SeriesIndexOverview extends Component {
const overviewHeight = contentHeight - titleRowHeight;
return (
<div className={styles.container}>
<div className={styles.container} style={style}>
<div className={styles.content}>
<div className={styles.poster}>
<div className={styles.posterContainer}>
@@ -241,6 +242,7 @@ class SeriesIndexOverview extends Component {
}
SeriesIndexOverview.propTypes = {
style: PropTypes.object.isRequired,
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired,

View File

@@ -1,11 +1,3 @@
.grid {
flex: 1 0 auto;
}
.container {
&:hover {
.content {
background-color: $tableRowHoverBackgroundColor;
}
}
}

View File

@@ -1,9 +1,12 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Grid, WindowScroller } from 'react-virtualized';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import dimensions from 'Styles/Variables/dimensions';
import { sortDirections } from 'Helpers/Props';
import Measure from 'Components/Measure';
import SeriesIndexItemConnector from 'Series/Index/SeriesIndexItemConnector';
import SeriesIndexOverview from './SeriesIndexOverview';
@@ -63,44 +66,56 @@ class SeriesIndexOverviews extends Component {
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {})
};
this._isInitialized = false;
this._grid = null;
}
componentDidUpdate(prevProps, prevState) {
componentDidMount() {
this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody);
}
componentDidUpdate(prevProps) {
const {
items,
filters,
sortKey,
sortDirection,
overviewOptions,
jumpToCharacter
} = this.props;
const {
width,
rowHeight
} = this.state;
const itemsChanged = hasDifferentItems(prevProps.items, items);
const overviewOptionsChanged = !_.isMatch(prevProps.overviewOptions, overviewOptions);
if (prevProps.sortKey !== sortKey ||
prevProps.overviewOptions !== overviewOptions) {
if (
prevProps.sortKey !== sortKey ||
prevProps.overviewOptions !== overviewOptions ||
itemsChanged
) {
this.calculateGrid();
}
if (this._grid &&
(prevState.width !== width ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
if (
prevProps.filters !== filters ||
prevProps.sortKey !== sortKey ||
prevProps.sortDirection !== sortDirection ||
itemsChanged ||
overviewOptionsChanged
) {
this._grid.recomputeGridSize();
}
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
if (this._grid && index != null) {
if (index != null) {
const {
rowHeight
} = this.state;
this._grid.scrollToCell({
rowIndex: index,
columnIndex: 0
});
const scrollTop = rowHeight * index;
this.props.onScroll({ scrollTop });
}
}
}
@@ -108,6 +123,21 @@ class SeriesIndexOverviews extends Component {
//
// Control
scrollToFirstCharacter(character) {
const items = this.props.items;
const {
rowHeight
} = this.state;
const index = getIndexOfFirstCharacter(items, character);
if (index != null) {
const scrollTop = rowHeight * index;
this.props.onScroll({ scrollTop });
}
}
setGridRef = (ref) => {
this._grid = ref;
}
@@ -155,30 +185,24 @@ class SeriesIndexOverviews extends Component {
}
return (
<div
className={styles.container}
<SeriesIndexItemConnector
key={key}
component={SeriesIndexOverview}
sortKey={sortKey}
posterWidth={posterWidth}
posterHeight={posterHeight}
rowHeight={rowHeight}
overviewOptions={overviewOptions}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
isSmallScreen={isSmallScreen}
style={style}
>
<SeriesIndexItemConnector
key={series.id}
component={SeriesIndexOverview}
sortKey={sortKey}
posterWidth={posterWidth}
posterHeight={posterHeight}
rowHeight={rowHeight}
overviewOptions={overviewOptions}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
isSmallScreen={isSmallScreen}
style={style}
seriesId={series.id}
languageProfileId={series.languageProfileId}
qualityProfileId={series.qualityProfileId}
/>
</div>
seriesId={series.id}
languageProfileId={series.languageProfileId}
qualityProfileId={series.qualityProfileId}
/>
);
}
@@ -189,14 +213,22 @@ class SeriesIndexOverviews extends Component {
this.calculateGrid(width, this.props.isSmallScreen);
}
onSectionRendered = () => {
if (!this._isInitialized && this._contentBodyNode) {
this.props.onRender();
this._isInitialized = true;
}
}
//
// Render
render() {
const {
scroller,
items,
isSmallScreen
scrollTop,
isSmallScreen,
onScroll
} = this.props;
const {
@@ -205,38 +237,28 @@ class SeriesIndexOverviews extends Component {
} = this.state;
return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<Measure onMeasure={this.onMeasure}>
<WindowScroller
scrollElement={isSmallScreen ? undefined : scroller}
scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
onScroll={onScroll}
>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return <div />;
}
{({ height, isScrolling }) => {
return (
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={1}
columnWidth={width}
rowCount={items.length}
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
scrollToAlignment={'start'}
isScrollingOptOut={true}
/>
</div>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={1}
columnWidth={width}
rowCount={items.length}
rowHeight={rowHeight}
width={width}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
onSectionRendered={this.onSectionRendered}
/>
);
}
}
@@ -248,15 +270,20 @@ class SeriesIndexOverviews extends Component {
SeriesIndexOverviews.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
overviewOptions: PropTypes.object.isRequired,
scrollTop: PropTypes.number.isRequired,
jumpToCharacter: PropTypes.string,
scroller: PropTypes.instanceOf(Element).isRequired,
contentBody: PropTypes.object.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired
timeFormat: PropTypes.string.isRequired,
onRender: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired
};
export default SeriesIndexOverviews;

View File

@@ -1,5 +1,9 @@
$hoverScale: 1.05;
.container {
padding: 10px;
}
.content {
transition: all 200ms ease-in;

View File

@@ -67,6 +67,7 @@ class SeriesIndexPoster extends Component {
render() {
const {
style,
id,
title,
monitored,
@@ -114,144 +115,147 @@ class SeriesIndexPoster extends Component {
};
return (
<div className={styles.content}>
<div className={styles.posterContainer}>
<Label className={styles.controls}>
<SpinnerIconButton
className={styles.action}
name={icons.REFRESH}
title="Refresh series"
isSpinning={isRefreshingSeries}
onPress={onRefreshSeriesPress}
/>
<div className={styles.container} style={style}>
<div className={styles.content}>
<div className={styles.posterContainer}>
<Label className={styles.controls}>
<SpinnerIconButton
className={styles.action}
name={icons.REFRESH}
title="Refresh series"
isSpinning={isRefreshingSeries}
onPress={onRefreshSeriesPress}
/>
{
showSearchAction &&
<SpinnerIconButton
className={styles.action}
name={icons.SEARCH}
title="Search for monitored episodes"
isSpinning={isSearchingSeries}
onPress={onSearchPress}
/>
}
<IconButton
className={styles.action}
name={icons.EDIT}
title="Edit Series"
onPress={this.onEditSeriesPress}
/>
</Label>
{
showSearchAction &&
<SpinnerIconButton
className={styles.action}
name={icons.SEARCH}
title="Search for monitored episodes"
isSpinning={isSearchingSeries}
onPress={onSearchPress}
status === 'ended' &&
<div
className={styles.ended}
title="Ended"
/>
}
<IconButton
className={styles.action}
name={icons.EDIT}
title="Edit Series"
onPress={this.onEditSeriesPress}
/>
</Label>
<Link
className={styles.link}
style={elementStyle}
to={link}
>
<SeriesPoster
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={this.onPosterLoadError}
onLoad={this.onPosterLoad}
/>
{
hasPosterError &&
<div className={styles.overlayTitle}>
{title}
</div>
}
</Link>
</div>
<SeriesIndexProgressBar
monitored={monitored}
status={status}
episodeCount={episodeCount}
episodeFileCount={episodeFileCount}
totalEpisodeCount={totalEpisodeCount}
posterWidth={posterWidth}
detailedProgressBar={detailedProgressBar}
/>
{
status === 'ended' &&
<div
className={styles.ended}
title="Ended"
/>
showTitle &&
<div className={styles.title}>
{title}
</div>
}
<Link
className={styles.link}
style={elementStyle}
to={link}
>
<SeriesPoster
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={this.onPosterLoadError}
onLoad={this.onPosterLoad}
/>
{
showMonitored &&
<div className={styles.title}>
{monitored ? 'Monitored' : 'Unmonitored'}
</div>
}
{
hasPosterError &&
<div className={styles.overlayTitle}>
{title}
</div>
}
</Link>
{
showQualityProfile &&
<div className={styles.title}>
{qualityProfile.name}
</div>
}
{
nextAiring &&
<div className={styles.nextAiring}>
{
getRelativeDate(
nextAiring,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: true
}
)
}
</div>
}
<SeriesIndexPosterInfo
seasonCount={seasonCount}
qualityProfile={qualityProfile}
showQualityProfile={showQualityProfile}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
{...otherProps}
/>
<EditSeriesModalConnector
isOpen={isEditSeriesModalOpen}
seriesId={id}
onModalClose={this.onEditSeriesModalClose}
onDeleteSeriesPress={this.onDeleteSeriesPress}
/>
<DeleteSeriesModal
isOpen={isDeleteSeriesModalOpen}
seriesId={id}
onModalClose={this.onDeleteSeriesModalClose}
/>
</div>
<SeriesIndexProgressBar
monitored={monitored}
status={status}
episodeCount={episodeCount}
episodeFileCount={episodeFileCount}
totalEpisodeCount={totalEpisodeCount}
posterWidth={posterWidth}
detailedProgressBar={detailedProgressBar}
/>
{
showTitle &&
<div className={styles.title}>
{title}
</div>
}
{
showMonitored &&
<div className={styles.title}>
{monitored ? 'Monitored' : 'Unmonitored'}
</div>
}
{
showQualityProfile &&
<div className={styles.title}>
{qualityProfile.name}
</div>
}
{
nextAiring &&
<div className={styles.nextAiring}>
{
getRelativeDate(
nextAiring,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: true
}
)
}
</div>
}
<SeriesIndexPosterInfo
seasonCount={seasonCount}
qualityProfile={qualityProfile}
showQualityProfile={showQualityProfile}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
{...otherProps}
/>
<EditSeriesModalConnector
isOpen={isEditSeriesModalOpen}
seriesId={id}
onModalClose={this.onEditSeriesModalClose}
onDeleteSeriesPress={this.onDeleteSeriesPress}
/>
<DeleteSeriesModal
isOpen={isDeleteSeriesModalOpen}
seriesId={id}
onModalClose={this.onDeleteSeriesModalClose}
/>
</div>
);
}
}
SeriesIndexPoster.propTypes = {
style: PropTypes.object.isRequired,
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,

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