mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-19 16:34:12 -04:00
Compare commits
167 Commits
phantom-ad
...
phantom-di
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94f8e38d5a | ||
|
|
031371652b | ||
|
|
02104aff34 | ||
|
|
5bd1c47ca7 | ||
|
|
f846e0c031 | ||
|
|
72b0f640f4 | ||
|
|
430af0401c | ||
|
|
0ff08dbe8d | ||
|
|
57cca9fcdc | ||
|
|
b3daa280c5 | ||
|
|
449c1caf55 | ||
|
|
0c05236bee | ||
|
|
9f54ff8169 | ||
|
|
f2e1b4e435 | ||
|
|
1e80361c3a | ||
|
|
3564be19a8 | ||
|
|
14c9b6aaf4 | ||
|
|
069fc5cd33 | ||
|
|
3586d7042b | ||
|
|
a32b6276bd | ||
|
|
b0e31629b5 | ||
|
|
e2644c3847 | ||
|
|
7ea45bb714 | ||
|
|
f524fcd3e4 | ||
|
|
6c418302f8 | ||
|
|
b28d329654 | ||
|
|
ebe2ad1520 | ||
|
|
41dfb677e7 | ||
|
|
c646bef369 | ||
|
|
910de6d94a | ||
|
|
5951992bd5 | ||
|
|
cb9d78064a | ||
|
|
fd608fd411 | ||
|
|
d3bd90e4b9 | ||
|
|
4988655568 | ||
|
|
098db08ede | ||
|
|
93e3e92bba | ||
|
|
bdfdd28d6a | ||
|
|
a75e10c4c9 | ||
|
|
5251db7224 | ||
|
|
4d1a4d4241 | ||
|
|
d3a22459ac | ||
|
|
4f7e00bdc4 | ||
|
|
1199ae4e4f | ||
|
|
36088ef49d | ||
|
|
7ffb2eb440 | ||
|
|
0716d0931a | ||
|
|
66ee28d0a9 | ||
|
|
1487f54749 | ||
|
|
013c46d266 | ||
|
|
c8d2fcb223 | ||
|
|
5288b61378 | ||
|
|
a2679f64ee | ||
|
|
5d9dfee3c0 | ||
|
|
98f9323b42 | ||
|
|
f282ae8aae | ||
|
|
0b1e99991e | ||
|
|
75be036a87 | ||
|
|
23dc7794f1 | ||
|
|
b8e2f3d716 | ||
|
|
686a14cdff | ||
|
|
b4405b0600 | ||
|
|
d4bcf28d08 | ||
|
|
4bacc35605 | ||
|
|
417340c2c6 | ||
|
|
79d8a9d44b | ||
|
|
68440bba4d | ||
|
|
0719c83da4 | ||
|
|
5a7dec34cc | ||
|
|
9d766cfed5 | ||
|
|
1498f4e361 | ||
|
|
05dd17aacb | ||
|
|
200aee52f7 | ||
|
|
d6dd13a6be | ||
|
|
be3b3df903 | ||
|
|
f2a56b29d9 | ||
|
|
27d98868b8 | ||
|
|
7f28ab895a | ||
|
|
97ec184754 | ||
|
|
42343d5283 | ||
|
|
479baf06a7 | ||
|
|
7f7d196e44 | ||
|
|
c862fd9ff6 | ||
|
|
770b89c2b3 | ||
|
|
576275b6da | ||
|
|
776191b3bd | ||
|
|
d369d85699 | ||
|
|
552fac0466 | ||
|
|
a348d98dd9 | ||
|
|
ccdfdd1049 | ||
|
|
f0ca636654 | ||
|
|
b5e734b9e5 | ||
|
|
e1639d35a2 | ||
|
|
9b99ad27cd | ||
|
|
bba57bb434 | ||
|
|
8c24cd9864 | ||
|
|
91de7ff11c | ||
|
|
9702d2e5ad | ||
|
|
638066db03 | ||
|
|
1b3839ac0d | ||
|
|
219494ea9d | ||
|
|
642f75761f | ||
|
|
ed28f94f02 | ||
|
|
618c611a59 | ||
|
|
00821b7ad6 | ||
|
|
84b9488cfb | ||
|
|
37ad801065 | ||
|
|
4219cdb364 | ||
|
|
e23a879669 | ||
|
|
4ddf4a22a3 | ||
|
|
72afb28c30 | ||
|
|
eb51a42f60 | ||
|
|
bc01384cc7 | ||
|
|
00c922875f | ||
|
|
8c93d73b42 | ||
|
|
3b6d60e904 | ||
|
|
a965b8e7b2 | ||
|
|
25abf52b3f | ||
|
|
19764014be | ||
|
|
c91a5c80d3 | ||
|
|
e7b88c313d | ||
|
|
9ac0864b61 | ||
|
|
fcdd0f21c7 | ||
|
|
5497b68a98 | ||
|
|
50886ac928 | ||
|
|
e2ff089232 | ||
|
|
ae7f8926f8 | ||
|
|
0bbc4e8c1b | ||
|
|
295fdad750 | ||
|
|
63e01aff8c | ||
|
|
e05ceb226c | ||
|
|
1c699841c1 | ||
|
|
385c7fb0ce | ||
|
|
15d84046db | ||
|
|
3ad396a9c2 | ||
|
|
77f886ceef | ||
|
|
8adb788205 | ||
|
|
d731317c81 | ||
|
|
a824ce691b | ||
|
|
506023b0f3 | ||
|
|
52e5d4d0f1 | ||
|
|
00edffc0f4 | ||
|
|
92f1f3e73a | ||
|
|
1d339ad4f1 | ||
|
|
99728a604d | ||
|
|
c07a67ae3c | ||
|
|
be11789a86 | ||
|
|
b8ce274fa5 | ||
|
|
d7967e3e1b | ||
|
|
746da69070 | ||
|
|
b05b7ec4ad | ||
|
|
9abdaca079 | ||
|
|
5a79b8502e | ||
|
|
466d4fba9e | ||
|
|
108f6fe393 | ||
|
|
792896c46b | ||
|
|
43d04cd54e | ||
|
|
283f905d79 | ||
|
|
dd8d1b673e | ||
|
|
9ef64660ce | ||
|
|
88b1c8fc3e | ||
|
|
bcc8b655f7 | ||
|
|
438d9eb717 | ||
|
|
2c0a0175ef | ||
|
|
e51f1b5e16 | ||
|
|
544108df37 | ||
|
|
a23639e62e |
85
README.md
85
README.md
@@ -1,58 +1,73 @@
|
||||
# Sonarr
|
||||
# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> 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.
|
||||
|
||||
## Major Features Include:
|
||||
## Getting Started
|
||||
|
||||
* 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
|
||||
- [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
|
||||
|
||||
## 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 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`
|
||||
- Make sure all the required software mentioned above are installed
|
||||
- Clone the repository recursively to get Sonarr and it's submodules
|
||||
- You can do this by running `git clone --recursive https://github.com/Sonarr/Sonarr.git`
|
||||
- Install the required Node Packages using `yarn`
|
||||
|
||||
### Backend Development
|
||||
|
||||
* Run `yarn build` to build the UI
|
||||
* Open `Sonarr.sln` in Visual Studio
|
||||
* Make sure `NzbDrone.Console` is set as the startup project
|
||||
* Build `NzbDrone.Windows` and `NzbDrone.Mono` projects
|
||||
* Build Solution
|
||||
- Run `yarn build` to build the UI
|
||||
- Open `Sonarr.sln` in Visual Studio
|
||||
- Make sure `Sonarr.Console` is set as the startup project
|
||||
- Build `Sonarr.Windows` and `Sonarr.Mono` projects
|
||||
- Build Solution
|
||||
|
||||
### UI Development
|
||||
|
||||
* Run `yarn watch` to build UI and rebuild automatically when changes are detected
|
||||
* Run Sonarr.Console.exe (or debug in Visual Studio)
|
||||
- Run `yarn watch` to build UI and rebuild automatically when changes are detected
|
||||
- Run Sonarr.Console.exe (or debug in Visual Studio)
|
||||
|
||||
### License
|
||||
### Licenses
|
||||
|
||||
|
||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
* Copyright 2010-2019
|
||||
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
- Copyright 2010-2020
|
||||
|
||||
### 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/)
|
||||
|
||||
@@ -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.4),
|
||||
libmono-cil-dev (>= 5.4),
|
||||
mono-devel (>= 5.18),
|
||||
libmono-cil-dev (>= 5.18),
|
||||
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.4), 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.18), 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
|
||||
|
||||
@@ -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 [ $1 = "upgrade" ] && [ "$UPDATER" = "BuiltIn" ]; then
|
||||
if [ "$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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
|
||||
EXCLUDE_MODULEREFS = crypt32 httpapi
|
||||
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll libmonosgen-2.0 clr mscorlib mscoree.dll Microsoft.DiaSymReader.Native.x86.dll Microsoft.DiaSymReader.Native.amd64.dll
|
||||
|
||||
%:
|
||||
dh $@ --with=systemd --with=cli
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
FROM ubuntu:xenial AS builder
|
||||
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
ENV MONO_VERSION 5.14
|
||||
ENV MONO_VERSION 5.18
|
||||
|
||||
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 && \
|
||||
|
||||
@@ -16,6 +16,7 @@ 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
|
||||
|
||||
@@ -2,16 +2,18 @@
|
||||
opt_parallel=
|
||||
opt_version=
|
||||
opt_mode=both
|
||||
while getopts 'pv:m:?h' c
|
||||
while getopts 'pv:m:r?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
|
||||
@@ -20,17 +22,22 @@ 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
|
||||
|
||||
# Preferred versions
|
||||
MONO_VERSIONS="6.8 6.6 6.4 6.0 5.20 5.18"
|
||||
MONO_VERSIONS=""
|
||||
|
||||
# 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.16 5.14 5.12 5.10 5.8 5.4"
|
||||
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"
|
||||
|
||||
# Legacy unsupported versions
|
||||
MONO_VERSIONS="$MONO_VERSIONS 5.0"
|
||||
MONO_VERSIONS="$MONO_VERSIONS 5.10 5.8 5.4 5.0"
|
||||
#MONO_VERSIONS="$MONO_VERSIONS 4.8=stable-wheezy/snapshots/4.8"
|
||||
|
||||
if [ "$opt_version" != "" ]; then
|
||||
@@ -86,23 +93,29 @@ runOne() {
|
||||
echo "Finished Test Docker for mono $MONO_VERSION"
|
||||
}
|
||||
|
||||
if [ "$opt_parallel" == "1" ]; then
|
||||
if [ "$opt_report" != "1" ]; then
|
||||
|
||||
if [ "$opt_parallel" == "1" ]; then
|
||||
for MONO_VERSION_PAIR in $MONO_VERSIONS; do
|
||||
prepOne "$MONO_VERSION_PAIR"
|
||||
done
|
||||
fi
|
||||
|
||||
for MONO_VERSION_PAIR in $MONO_VERSIONS; do
|
||||
prepOne "$MONO_VERSION_PAIR"
|
||||
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
|
||||
|
||||
fi
|
||||
|
||||
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
|
||||
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.
|
||||
@@ -10,8 +10,7 @@ gulp.task('build',
|
||||
'webpack',
|
||||
'copyHtml',
|
||||
'copyFonts',
|
||||
'copyImages',
|
||||
'copyJs'
|
||||
'copyImages'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -5,17 +5,6 @@ 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'))
|
||||
|
||||
@@ -6,17 +6,21 @@ 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',
|
||||
@@ -113,6 +117,22 @@ 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)/,
|
||||
@@ -213,6 +233,24 @@ 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'));
|
||||
|
||||
20
frontend/jsconfig.json
Normal file
20
frontend/jsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"checkJs": false,
|
||||
"baseUrl": "src",
|
||||
"jsx": "react",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"*": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
]
|
||||
}
|
||||
@@ -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 PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
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';
|
||||
@@ -56,7 +56,7 @@ class Blacklist extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -103,7 +103,7 @@ class Blacklist extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
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';
|
||||
@@ -96,7 +96,7 @@ class History extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetchingAny && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -147,7 +147,7 @@ class History extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -12,7 +13,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 PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
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';
|
||||
@@ -36,34 +37,26 @@ class Queue extends Component {
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isPendingSelected: false,
|
||||
isConfirmRemoveModalOpen: false
|
||||
isConfirmRemoveModalOpen: false,
|
||||
items: props.items
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
// Don't update when fetching has completed if items have changed,
|
||||
// before episodes start fetching or when episodes start fetching.
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
isEpisodesFetching
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
this.props.isFetching &&
|
||||
nextProps.isPopulated &&
|
||||
hasDifferentItems(this.props.items, nextProps.items) &&
|
||||
nextProps.items.some((e) => e.episodeId)
|
||||
(!isEpisodesFetching && prevProps.isEpisodesFetching) ||
|
||||
(hasDifferentItems(prevProps.items, items) && !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, prevProps.items);
|
||||
return {
|
||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||
items
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -124,7 +117,6 @@ class Queue extends Component {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
isEpisodesFetching,
|
||||
isEpisodesPopulated,
|
||||
episodesError,
|
||||
@@ -142,7 +134,8 @@ class Queue extends Component {
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmRemoveModalOpen,
|
||||
isPendingSelected
|
||||
isPendingSelected,
|
||||
items
|
||||
} = this.state;
|
||||
|
||||
const isRefreshing = isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||
@@ -198,7 +191,7 @@ class Queue extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isRefreshing && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -255,7 +248,7 @@ class Queue extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
<RemoveQueueItemsModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
|
||||
@@ -10,13 +10,13 @@ function QueueDetails(props) {
|
||||
size,
|
||||
sizeleft,
|
||||
estimatedCompletionTime,
|
||||
status: queueStatus,
|
||||
status,
|
||||
trackedDownloadState,
|
||||
trackedDownloadStatus,
|
||||
errorMessage,
|
||||
progressBar
|
||||
} = props;
|
||||
|
||||
const status = queueStatus.toLowerCase();
|
||||
|
||||
const progress = (100 - sizeleft / size * 100);
|
||||
|
||||
if (status === 'pending') {
|
||||
@@ -39,7 +39,35 @@ function QueueDetails(props) {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: show an icon when download is complete, but not imported yet?
|
||||
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'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
@@ -90,6 +118,8 @@ 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
|
||||
};
|
||||
|
||||
@@ -222,9 +222,13 @@ class QueueRow extends Component {
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
/>
|
||||
{
|
||||
quality ?
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -352,7 +356,7 @@ class QueueRow extends Component {
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canIgnore={!!(series && episode)}
|
||||
canIgnore={!!series}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -15,11 +14,11 @@ function createMapStateToProps() {
|
||||
createEpisodeSelector(),
|
||||
createUISettingsSelector(),
|
||||
(series, episode, uiSettings) => {
|
||||
const result = _.pick(uiSettings, [
|
||||
'showRelativeDates',
|
||||
'shortDateFormat',
|
||||
'timeFormat'
|
||||
]);
|
||||
const result = {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
|
||||
result.series = series;
|
||||
result.episode = episode;
|
||||
|
||||
@@ -51,10 +51,6 @@ function QueueStatusCell(props) {
|
||||
let iconKind = kinds.DEFAULT;
|
||||
let title = 'Downloading';
|
||||
|
||||
if (hasWarning) {
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (status === 'paused') {
|
||||
iconName = icons.PAUSED;
|
||||
title = 'Paused';
|
||||
@@ -71,17 +67,24 @@ 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';
|
||||
|
||||
@@ -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) {
|
||||
if (!timeleft || status === 'completed' || status === 'failed') {
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
-
|
||||
|
||||
@@ -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 PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import AddNewSeriesSearchResultConnector from './AddNewSeriesSearchResultConnector';
|
||||
import styles from './AddNewSeries.css';
|
||||
|
||||
@@ -88,7 +88,7 @@ class AddNewSeries extends Component {
|
||||
|
||||
return (
|
||||
<PageContent title="Add New Series">
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.searchIconContainer}>
|
||||
<Icon
|
||||
@@ -191,7 +191,7 @@ class AddNewSeries extends Component {
|
||||
}
|
||||
|
||||
<div />
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
.alreadyExistsIcon {
|
||||
margin-left: 10px;
|
||||
color: #37bc9b;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.overview {
|
||||
|
||||
@@ -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 PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import ImportSeriesTableConnector from './ImportSeriesTableConnector';
|
||||
import ImportSeriesFooterConnector from './ImportSeriesFooterConnector';
|
||||
|
||||
@@ -21,17 +21,15 @@ class ImportSeries extends Component {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
contentBody: null,
|
||||
scrollTop: 0
|
||||
selectedState: {}
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setContentBodyRef = (ref) => {
|
||||
this.setState({ contentBody: ref });
|
||||
setScrollerRef = (ref) => {
|
||||
this.setState({ scroller: ref });
|
||||
}
|
||||
|
||||
//
|
||||
@@ -94,13 +92,13 @@ class ImportSeries extends Component {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
contentBody
|
||||
scroller
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<PageContent title="Import Series">
|
||||
<PageContentBodyConnector
|
||||
ref={this.setContentBodyRef}
|
||||
<PageContentBody
|
||||
registerScroller={this.setScrollerRef}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
{
|
||||
@@ -121,23 +119,21 @@ class ImportSeries extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && contentBody &&
|
||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && scroller &&
|
||||
<ImportSeriesTableConnector
|
||||
rootFolderId={rootFolderId}
|
||||
unmappedFolders={unmappedFolders}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
selectedState={selectedState}
|
||||
contentBody={contentBody}
|
||||
scroller={scroller}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
scrollTop={this.state.scrollTop}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
|
||||
onScroll={this.onScroll}
|
||||
/>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length &&
|
||||
|
||||
@@ -2,7 +2,6 @@ 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';
|
||||
@@ -10,7 +9,6 @@ import styles from './ImportSeriesRow.css';
|
||||
|
||||
function ImportSeriesRow(props) {
|
||||
const {
|
||||
style,
|
||||
id,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
@@ -26,7 +24,7 @@ function ImportSeriesRow(props) {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<VirtualTableRow style={style}>
|
||||
<>
|
||||
<VirtualTableSelectCell
|
||||
inputClassName={styles.selectInput}
|
||||
id={id}
|
||||
@@ -93,12 +91,11 @@ 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,
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
|
||||
@@ -112,15 +113,19 @@ class ImportSeriesTable extends Component {
|
||||
const item = items[rowIndex];
|
||||
|
||||
return (
|
||||
<ImportSeriesRowConnector
|
||||
<VirtualTableRow
|
||||
key={key}
|
||||
style={style}
|
||||
rootFolderId={rootFolderId}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
id={item.id}
|
||||
/>
|
||||
>
|
||||
<ImportSeriesRowConnector
|
||||
key={item.id}
|
||||
rootFolderId={rootFolderId}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
id={item.id}
|
||||
/>
|
||||
</VirtualTableRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,12 +138,10 @@ class ImportSeriesTable extends Component {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
isSmallScreen,
|
||||
contentBody,
|
||||
scroller,
|
||||
showLanguageProfile,
|
||||
scrollTop,
|
||||
selectedState,
|
||||
onSelectAllChange,
|
||||
onScroll
|
||||
onSelectAllChange
|
||||
} = this.props;
|
||||
|
||||
if (!items.length) {
|
||||
@@ -148,10 +151,9 @@ class ImportSeriesTable extends Component {
|
||||
return (
|
||||
<VirtualTable
|
||||
items={items}
|
||||
contentBody={contentBody}
|
||||
scroller={scroller}
|
||||
isSmallScreen={isSmallScreen}
|
||||
rowHeight={52}
|
||||
scrollTop={scrollTop}
|
||||
overscanRowCount={2}
|
||||
rowRenderer={this.rowRenderer}
|
||||
header={
|
||||
@@ -163,7 +165,6 @@ class ImportSeriesTable extends Component {
|
||||
/>
|
||||
}
|
||||
selectedState={selectedState}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -183,15 +184,13 @@ ImportSeriesTable.propTypes = {
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
allSeries: PropTypes.arrayOf(PropTypes.object),
|
||||
contentBody: PropTypes.object.isRequired,
|
||||
scroller: PropTypes.instanceOf(Element).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,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
onSetImportSeriesValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportSeriesTable;
|
||||
|
||||
@@ -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 PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import RootFolders from 'RootFolder/RootFolders';
|
||||
import styles from './ImportSeriesSelectFolder.css';
|
||||
|
||||
@@ -53,7 +53,7 @@ class ImportSeriesSelectFolder extends Component {
|
||||
|
||||
return (
|
||||
<PageContent title="Import Series">
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -132,7 +132,7 @@ class ImportSeriesSelectFolder extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ 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 {
|
||||
@@ -229,6 +230,15 @@ function AppRoutes(props) {
|
||||
component={Logs}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Diagnostics
|
||||
*/}
|
||||
|
||||
<Route
|
||||
path="/diag"
|
||||
component={Diagnostic}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Not Found
|
||||
*/}
|
||||
|
||||
@@ -3,9 +3,10 @@ import React, { Component } from 'react';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import Measure from 'Components/Measure';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
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';
|
||||
@@ -76,8 +77,10 @@ class CalendarPage extends Component {
|
||||
filters,
|
||||
hasSeries,
|
||||
missingEpisodeIds,
|
||||
isRssSyncExecuting,
|
||||
isSearchingForMissing,
|
||||
useCurrentPage,
|
||||
onRssSyncPress,
|
||||
onFilterSelect
|
||||
} = this.props;
|
||||
|
||||
@@ -99,6 +102,15 @@ 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}
|
||||
@@ -126,7 +138,7 @@ class CalendarPage extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector
|
||||
<PageContentBody
|
||||
className={styles.calendarPageBody}
|
||||
innerClassName={styles.calendarInnerPageBody}
|
||||
>
|
||||
@@ -147,7 +159,7 @@ class CalendarPage extends Component {
|
||||
hasSeries &&
|
||||
<LegendConnector />
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
<CalendarLinkModal
|
||||
isOpen={isCalendarLinkModalOpen}
|
||||
@@ -168,10 +180,12 @@ 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
|
||||
};
|
||||
|
||||
|
||||
@@ -3,11 +3,14 @@ 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() {
|
||||
@@ -59,6 +62,7 @@ function createMapStateToProps() {
|
||||
createSeriesCountSelector(),
|
||||
createUISettingsSelector(),
|
||||
createMissingEpisodeIdsSelector(),
|
||||
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
||||
createIsSearchingSelector(),
|
||||
(
|
||||
selectedFilterKey,
|
||||
@@ -66,6 +70,7 @@ function createMapStateToProps() {
|
||||
seriesCount,
|
||||
uiSettings,
|
||||
missingEpisodeIds,
|
||||
isRssSyncExecuting,
|
||||
isSearchingForMissing
|
||||
) => {
|
||||
return {
|
||||
@@ -74,6 +79,7 @@ function createMapStateToProps() {
|
||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||
hasSeries: !!seriesCount,
|
||||
missingEpisodeIds,
|
||||
isRssSyncExecuting,
|
||||
isSearchingForMissing
|
||||
};
|
||||
}
|
||||
@@ -82,6 +88,12 @@ function createMapStateToProps() {
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRssSyncPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.RSS_SYNC
|
||||
}));
|
||||
},
|
||||
|
||||
onSearchMissingPress(episodeIds) {
|
||||
dispatch(searchMissing({ episodeIds }));
|
||||
},
|
||||
|
||||
@@ -11,10 +11,12 @@ function CalendarEventQueueDetails(props) {
|
||||
sizeleft,
|
||||
estimatedCompletionTime,
|
||||
status,
|
||||
trackedDownloadState,
|
||||
trackedDownloadStatus,
|
||||
errorMessage
|
||||
} = props;
|
||||
|
||||
const progress = (100 - sizeleft / size * 100);
|
||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||
|
||||
return (
|
||||
<QueueDetails
|
||||
@@ -23,6 +25,8 @@ 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}`}>
|
||||
@@ -44,6 +48,8 @@ CalendarEventQueueDetails.propTypes = {
|
||||
sizeleft: PropTypes.number.isRequired,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
errorMessage: PropTypes.string
|
||||
};
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ class FilterBuilderModalContent extends Component {
|
||||
filters.map((filter, index) => {
|
||||
return (
|
||||
<FilterBuilderRow
|
||||
key={index}
|
||||
key={`${filter.key}-${index}`}
|
||||
index={index}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
|
||||
@@ -135,7 +135,7 @@ class FilterBuilderRowValue extends Component {
|
||||
tagList={tagList}
|
||||
allowNew={!tagList.length}
|
||||
kind={kinds.DEFAULT}
|
||||
delimiters={[9, 13]}
|
||||
delimiters={['Tab', 'Enter']}
|
||||
maxSuggestionsLength={100}
|
||||
minQueryLength={0}
|
||||
tagComponent={FilterBuilderRowValueTag}
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
.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 {
|
||||
@@ -56,6 +45,7 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 90%;
|
||||
max-height: 100%;
|
||||
width: 350px !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
@@ -56,16 +56,6 @@ 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 });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -98,7 +98,9 @@ class KeyValueListInput extends Component {
|
||||
className,
|
||||
value,
|
||||
keyPlaceholder,
|
||||
valuePlaceholder
|
||||
valuePlaceholder,
|
||||
hasError,
|
||||
hasWarning
|
||||
} = this.props;
|
||||
|
||||
const { isFocused } = this.state;
|
||||
@@ -106,7 +108,9 @@ class KeyValueListInput extends Component {
|
||||
return (
|
||||
<div className={classNames(
|
||||
className,
|
||||
isFocused && styles.isFocused
|
||||
isFocused && styles.isFocused,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning
|
||||
)}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -8,7 +8,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
flex: 0 0 22px;
|
||||
}
|
||||
|
||||
.keyInput,
|
||||
.valueInput {
|
||||
width: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
@@ -63,34 +63,41 @@ class KeyValueListInputItem extends Component {
|
||||
|
||||
return (
|
||||
<div className={styles.itemContainer}>
|
||||
<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.keyInput}
|
||||
name="key"
|
||||
value={keyValue}
|
||||
placeholder={keyPlaceholder}
|
||||
onChange={this.onKeyChange}
|
||||
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.inputWrapper}>
|
||||
<TextInput
|
||||
className={styles.valueInput}
|
||||
name="value"
|
||||
value={value}
|
||||
placeholder={valuePlaceholder}
|
||||
onChange={this.onValueChange}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{
|
||||
!isNew &&
|
||||
<IconButton
|
||||
name={icons.REMOVE}
|
||||
tabIndex={-1}
|
||||
onPress={this.onRemovePress}
|
||||
/>
|
||||
}
|
||||
<div className={styles.buttonWrapper}>
|
||||
{
|
||||
isNew ?
|
||||
null :
|
||||
<IconButton
|
||||
name={icons.REMOVE}
|
||||
tabIndex={-1}
|
||||
onPress={this.onRemovePress}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,15 +4,16 @@ 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(
|
||||
(state) => state.settings.languageProfiles,
|
||||
createSortedSectionSelector('settings.languageProfiles', sortByName),
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(state, { includeMixed }) => includeMixed,
|
||||
(languageProfiles, includeNoChange, includeMixed) => {
|
||||
const values = _.map(languageProfiles.items.sort(sortByName), (languageProfile) => {
|
||||
const values = _.map(languageProfiles.items, (languageProfile) => {
|
||||
return {
|
||||
key: languageProfile.id,
|
||||
value: languageProfile.name
|
||||
|
||||
@@ -4,15 +4,16 @@ 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(
|
||||
(state) => state.settings.qualityProfiles,
|
||||
createSortedSectionSelector('settings.qualityProfiles', sortByName),
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(state, { includeMixed }) => includeMixed,
|
||||
(qualityProfiles, includeNoChange, includeMixed) => {
|
||||
const values = _.map(qualityProfiles.items.sort(sortByName), (qualityProfile) => {
|
||||
const values = _.map(qualityProfiles.items, (qualityProfile) => {
|
||||
return {
|
||||
key: qualityProfile.id,
|
||||
value: qualityProfile.name
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.hasError {
|
||||
composes: hasError from '~Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.hasWarning {
|
||||
composes: hasWarning from '~Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.internalInput {
|
||||
flex: 1 1 0%;
|
||||
margin-left: 3px;
|
||||
|
||||
@@ -100,9 +100,9 @@ class TagInput extends Component {
|
||||
suggestions
|
||||
} = this.state;
|
||||
|
||||
const keyCode = event.keyCode;
|
||||
const key = event.key;
|
||||
|
||||
if (keyCode === 8 && !value.length) {
|
||||
if (key === 'Backspace' && !value.length) {
|
||||
const index = tags.length - 1;
|
||||
|
||||
if (index >= 0) {
|
||||
@@ -116,7 +116,7 @@ class TagInput extends Component {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (delimiters.includes(keyCode)) {
|
||||
if (delimiters.includes(key)) {
|
||||
const selectedIndex = this._autosuggestRef.highlightedSuggestionIndex;
|
||||
const tag = getTag(value, selectedIndex, suggestions, allowNew);
|
||||
|
||||
@@ -210,6 +210,8 @@ class TagInput extends Component {
|
||||
const {
|
||||
className,
|
||||
inputContainerClassName,
|
||||
hasError,
|
||||
hasWarning,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -226,7 +228,9 @@ class TagInput extends Component {
|
||||
className={styles.internalInput}
|
||||
inputContainerClassName={classNames(
|
||||
inputContainerClassName,
|
||||
isFocused && styles.isFocused
|
||||
isFocused && styles.isFocused,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning
|
||||
)}
|
||||
value={value}
|
||||
suggestions={suggestions}
|
||||
@@ -256,7 +260,7 @@ TagInput.propTypes = {
|
||||
allowNew: PropTypes.bool.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
delimiters: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
delimiters: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
minQueryLength: PropTypes.number.isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
@@ -271,8 +275,7 @@ TagInput.defaultProps = {
|
||||
allowNew: true,
|
||||
kind: kinds.INFO,
|
||||
placeholder: '',
|
||||
// Tab, enter, space and comma
|
||||
delimiters: [9, 13, 32, 188],
|
||||
delimiters: ['Tab', 'Enter', ' ', ','],
|
||||
minQueryLength: 1,
|
||||
tagComponent: TagInputTag
|
||||
};
|
||||
|
||||
@@ -25,3 +25,7 @@
|
||||
.warning {
|
||||
color: $warningColor;
|
||||
}
|
||||
|
||||
.purple {
|
||||
color: $purple;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import styles from './LoadingIndicator.css';
|
||||
|
||||
function LoadingIndicator({ className, size }) {
|
||||
function LoadingIndicator({ className, rippleClassName, size }) {
|
||||
const sizeInPx = `${size}px`;
|
||||
const width = sizeInPx;
|
||||
const height = sizeInPx;
|
||||
@@ -17,17 +17,17 @@ function LoadingIndicator({ className, size }) {
|
||||
style={{ width, height }}
|
||||
>
|
||||
<div
|
||||
className={styles.ripple}
|
||||
className={rippleClassName}
|
||||
style={{ width, height }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={styles.ripple}
|
||||
className={rippleClassName}
|
||||
style={{ width, height }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={styles.ripple}
|
||||
className={rippleClassName}
|
||||
style={{ width, height }}
|
||||
/>
|
||||
</div>
|
||||
@@ -37,11 +37,13 @@ function LoadingIndicator({ className, size }) {
|
||||
|
||||
LoadingIndicator.propTypes = {
|
||||
className: PropTypes.string,
|
||||
rippleClassName: PropTypes.string,
|
||||
size: PropTypes.number
|
||||
};
|
||||
|
||||
LoadingIndicator.defaultProps = {
|
||||
className: styles.loading,
|
||||
rippleClassName: styles.ripple,
|
||||
size: 50
|
||||
};
|
||||
|
||||
|
||||
45
frontend/src/Components/Markdown/InlineMarkdown.js
Normal file
45
frontend/src/Components/Markdown/InlineMarkdown.js
Normal file
@@ -0,0 +1,45 @@
|
||||
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;
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import React, { useEffect } 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';
|
||||
@@ -21,9 +22,19 @@ function ConfirmModal(props) {
|
||||
hideCancelButton,
|
||||
isSpinning,
|
||||
onConfirm,
|
||||
onCancel
|
||||
onCancel,
|
||||
bindShortcut,
|
||||
unbindShortcut
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
bindShortcut('enter', onConfirm);
|
||||
} else {
|
||||
unbindShortcut('enter', onConfirm);
|
||||
}
|
||||
}, [onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@@ -49,7 +60,7 @@ function ConfirmModal(props) {
|
||||
}
|
||||
|
||||
<SpinnerButton
|
||||
data-autofocus={true}
|
||||
autoFocus={true}
|
||||
kind={kind}
|
||||
isSpinning={isSpinning}
|
||||
onPress={onConfirm}
|
||||
@@ -74,7 +85,9 @@ ConfirmModal.propTypes = {
|
||||
hideCancelButton: PropTypes.bool,
|
||||
isSpinning: PropTypes.bool.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
bindShortcut: PropTypes.func.isRequired,
|
||||
unbindShortcut: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ConfirmModal.defaultProps = {
|
||||
@@ -85,4 +98,4 @@ ConfirmModal.defaultProps = {
|
||||
isSpinning: false
|
||||
};
|
||||
|
||||
export default ConfirmModal;
|
||||
export default keyboardShortcuts(ConfirmModal);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -181,31 +182,33 @@ class Modal extends Component {
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
className={styles.modalContainer}
|
||||
>
|
||||
<FocusLock disabled={false}>
|
||||
<div
|
||||
ref={this._setBackgroundRef}
|
||||
className={backdropClassName}
|
||||
onMouseDown={this.onBackdropBeginPress}
|
||||
onMouseUp={this.onBackdropEndPress}
|
||||
className={styles.modalContainer}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
styles[size]
|
||||
)}
|
||||
style={style}
|
||||
ref={this._setBackgroundRef}
|
||||
className={backdropClassName}
|
||||
onMouseDown={this.onBackdropBeginPress}
|
||||
onMouseUp={this.onBackdropEndPress}
|
||||
>
|
||||
<ErrorBoundary
|
||||
errorComponent={ModalError}
|
||||
onModalClose={onModalClose}
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
styles[size]
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
<ErrorBoundary
|
||||
errorComponent={ModalError}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
</FocusLock>,
|
||||
this._node
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
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;
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
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 fuseOptions = {
|
||||
shouldSort: true,
|
||||
includeMatches: true,
|
||||
threshold: 0.3,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
keys: [
|
||||
'title',
|
||||
'alternateTitles.title',
|
||||
'tags.label'
|
||||
]
|
||||
};
|
||||
const workerInstance = new FuseWorker();
|
||||
|
||||
class SeriesSearchInput extends Component {
|
||||
|
||||
@@ -43,6 +32,7 @@ class SeriesSearchInput extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.bindShortcut(shortcuts.SERIES_SEARCH_INPUT.key, this.focusInput);
|
||||
workerInstance.addEventListener('message', this.onSuggestionsReceived, false);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -82,6 +72,16 @@ class SeriesSearchInput extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === LOADING_TYPE) {
|
||||
return (
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
rippleClassName={styles.ripple}
|
||||
size={30}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SeriesSearchResult
|
||||
{...item.item}
|
||||
@@ -154,35 +154,30 @@ class SeriesSearchInput extends Component {
|
||||
}
|
||||
|
||||
onSuggestionsFetchRequested = ({ 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.setState({
|
||||
suggestions: [
|
||||
{
|
||||
type: LOADING_TYPE,
|
||||
title: value
|
||||
}
|
||||
]
|
||||
});
|
||||
this.requestSuggestions(value);
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
} else {
|
||||
const fuse = new Fuse(series, fuseOptions);
|
||||
suggestions = fuse.search(value);
|
||||
}
|
||||
requestSuggestions = _.debounce((value) => {
|
||||
const payload = {
|
||||
value,
|
||||
series: this.props.series
|
||||
};
|
||||
|
||||
this.setState({ suggestions });
|
||||
workerInstance.postMessage(payload);
|
||||
}, 250);
|
||||
|
||||
onSuggestionsReceived = (message) => {
|
||||
this.setState({
|
||||
suggestions: message.data
|
||||
});
|
||||
}
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
|
||||
63
frontend/src/Components/Page/Header/fuse.worker.js
Normal file
63
frontend/src/Components/Page/Header/fuse.worker.js
Normal file
@@ -0,0 +1,63 @@
|
||||
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));
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -8,6 +9,15 @@ import styles from './PageContentBody.css';
|
||||
|
||||
class PageContentBody extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._isMobile = isMobileUtil();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -26,13 +36,12 @@ class PageContentBody extends Component {
|
||||
const {
|
||||
className,
|
||||
innerClassName,
|
||||
isSmallScreen,
|
||||
children,
|
||||
dispatch,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const ScrollerComponent = isSmallScreen ? Scroller : OverlayScroller;
|
||||
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
|
||||
|
||||
return (
|
||||
<ScrollerComponent
|
||||
@@ -52,7 +61,6 @@ 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
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
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);
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
|
||||
import PageContentBodyConnector from './PageContentBodyConnector';
|
||||
import PageContentBody from './PageContentBody';
|
||||
import styles from './PageContentError.css';
|
||||
|
||||
function PageContentError(props) {
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
<ErrorBoundaryError
|
||||
{...props}
|
||||
message='There was an error loading this page'
|
||||
/>
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
@@ -18,7 +17,7 @@ class PageJumpBar extends Component {
|
||||
|
||||
this.state = {
|
||||
height: 0,
|
||||
visibleItems: props.items
|
||||
visibleItems: props.items.order
|
||||
};
|
||||
}
|
||||
|
||||
@@ -52,29 +51,47 @@ 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 = items.length - maximumItems;
|
||||
const diff = order.length - maximumItems;
|
||||
|
||||
if (diff < 0) {
|
||||
this.setState({ visibleItems: items });
|
||||
this.setState({ visibleItems: order });
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.length < minimumItems) {
|
||||
this.setState({ visibleItems: items });
|
||||
if (order.length < minimumItems) {
|
||||
this.setState({ visibleItems: order });
|
||||
return;
|
||||
}
|
||||
|
||||
const removeDiff = Math.ceil(items.length / maximumItems);
|
||||
// get first, last, and most common in between to make up numbers
|
||||
const visibleItems = [order[0]];
|
||||
|
||||
const visibleItems = _.reduce(items, (acc, item, index) => {
|
||||
if (index % removeDiff === 0) {
|
||||
acc.push(item);
|
||||
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--;
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
visibleItems.push(order[order.length - 1]);
|
||||
|
||||
this.setState({ visibleItems });
|
||||
}
|
||||
@@ -129,7 +146,7 @@ class PageJumpBar extends Component {
|
||||
}
|
||||
|
||||
PageJumpBar.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
items: PropTypes.object.isRequired,
|
||||
minimumItems: PropTypes.number.isRequired,
|
||||
onItemPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.jumpBarItem {
|
||||
flex: 1 0 $jumpBarItemHeight;
|
||||
flex: 1 1 $jumpBarItemHeight;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
|
||||
@@ -165,6 +165,23 @@ 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'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -473,6 +490,10 @@ 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}
|
||||
|
||||
@@ -41,6 +41,7 @@ function PageToolbarButton(props) {
|
||||
}
|
||||
|
||||
PageToolbarButton.propTypes = {
|
||||
...Link.propTypes,
|
||||
label: PropTypes.string.isRequired,
|
||||
iconName: PropTypes.object.isRequired,
|
||||
spinningName: PropTypes.object,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.sectionContainer {
|
||||
display: flex;
|
||||
flex: 1 1 10%;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ 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;
|
||||
@@ -23,9 +22,7 @@ function calculateOverflowItems(children, isMeasured, width, collapseButtons) {
|
||||
const validChildren = [];
|
||||
|
||||
forEach(children, (child) => {
|
||||
const name = child.type.name;
|
||||
|
||||
if (name === SEPARATOR_NAME) {
|
||||
if (Object.keys(child.props).length === 0) {
|
||||
separatorCount++;
|
||||
} else {
|
||||
buttonCount++;
|
||||
@@ -68,12 +65,14 @@ function calculateOverflowItems(children, isMeasured, width, collapseButtons) {
|
||||
}
|
||||
|
||||
validChildren.forEach((child, index) => {
|
||||
const isSeparator = Object.keys(child.props).length === 0;
|
||||
|
||||
if (actualButtons < maxButtons) {
|
||||
if (child.type.name !== SEPARATOR_NAME) {
|
||||
if (!isSeparator) {
|
||||
buttons.push(child);
|
||||
actualButtons++;
|
||||
}
|
||||
} else if (child.type.name !== SEPARATOR_NAME) {
|
||||
} else if (!isSeparator) {
|
||||
overflowItems.push(child.props);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
/* Placeholder */
|
||||
}
|
||||
|
||||
.track {
|
||||
/* Placeholder */
|
||||
}
|
||||
|
||||
.thumb {
|
||||
min-height: 100px;
|
||||
border: 1px solid transparent;
|
||||
|
||||
@@ -37,6 +37,10 @@ class OverlayScroller extends Component {
|
||||
|
||||
_setScrollRef = (ref) => {
|
||||
this._scroller = ref;
|
||||
|
||||
if (ref) {
|
||||
this.props.registerScroller(ref.view);
|
||||
}
|
||||
}
|
||||
|
||||
_renderThumb = (props) => {
|
||||
@@ -60,6 +64,7 @@ class OverlayScroller extends Component {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.track}
|
||||
style={finalStyle}
|
||||
{...props}
|
||||
/>
|
||||
@@ -78,6 +83,7 @@ class OverlayScroller extends Component {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.track}
|
||||
style={finalStyle}
|
||||
{...props}
|
||||
/>
|
||||
@@ -157,7 +163,8 @@ OverlayScroller.propTypes = {
|
||||
autoHide: PropTypes.bool.isRequired,
|
||||
autoScroll: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node,
|
||||
onScroll: PropTypes.func
|
||||
onScroll: PropTypes.func,
|
||||
registerScroller: PropTypes.func
|
||||
};
|
||||
|
||||
OverlayScroller.defaultProps = {
|
||||
@@ -165,7 +172,8 @@ OverlayScroller.defaultProps = {
|
||||
trackClassName: styles.thumb,
|
||||
scrollDirection: scrollDirections.VERTICAL,
|
||||
autoHide: false,
|
||||
autoScroll: true
|
||||
autoScroll: true,
|
||||
registerScroller: () => {}
|
||||
};
|
||||
|
||||
export default OverlayScroller;
|
||||
|
||||
@@ -17,12 +17,18 @@ 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 });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -30,6 +36,8 @@ class Scroller extends Component {
|
||||
|
||||
_setScrollerRef = (ref) => {
|
||||
this._scroller = ref;
|
||||
|
||||
this.props.registerScroller(ref);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -43,6 +51,7 @@ class Scroller extends Component {
|
||||
children,
|
||||
scrollTop,
|
||||
onScroll,
|
||||
registerScroller,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -55,6 +64,7 @@ class Scroller extends Component {
|
||||
styles[scrollDirection],
|
||||
autoScroll && styles.autoScroll
|
||||
)}
|
||||
tabIndex={-1}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
@@ -67,15 +77,19 @@ 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
|
||||
onScroll: PropTypes.func,
|
||||
registerScroller: PropTypes.func
|
||||
};
|
||||
|
||||
Scroller.defaultProps = {
|
||||
scrollDirection: scrollDirections.VERTICAL,
|
||||
autoScroll: true
|
||||
autoFocus: true,
|
||||
autoScroll: true,
|
||||
registerScroller: () => {}
|
||||
};
|
||||
|
||||
export default Scroller;
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
.tableContainer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tableBodyContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
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 VirtualTableBody from './VirtualTableBody';
|
||||
import { WindowScroller, Grid } from 'react-virtualized';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import styles from './VirtualTable.css';
|
||||
|
||||
const ROW_HEIGHT = 38;
|
||||
@@ -44,28 +42,37 @@ class VirtualTable extends Component {
|
||||
width: 0
|
||||
};
|
||||
|
||||
this._isInitialized = false;
|
||||
this._grid = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody);
|
||||
}
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
items,
|
||||
scrollIndex
|
||||
} = this.props;
|
||||
|
||||
componentDidUpdate(prevProps, preState) {
|
||||
const { scrollIndex, rowHeight } = this.props;
|
||||
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();
|
||||
}
|
||||
|
||||
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
|
||||
const scrollTop = (scrollIndex + 1) * rowHeight + 20;
|
||||
|
||||
this.props.onScroll({ scrollTop });
|
||||
this._grid.scrollToCell({
|
||||
rowIndex: scrollIndex,
|
||||
columnIndex: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
rowGetter = ({ index }) => {
|
||||
return this.props.items[index];
|
||||
setGridRef = (ref) => {
|
||||
this._grid = ref;
|
||||
}
|
||||
|
||||
//
|
||||
@@ -77,36 +84,18 @@ 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,
|
||||
isSmallScreen,
|
||||
scroller,
|
||||
header,
|
||||
headerHeight,
|
||||
scrollTop,
|
||||
rowRenderer,
|
||||
onScroll,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -114,66 +103,89 @@ 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 (
|
||||
<Measure onMeasure={this.onMeasure}>
|
||||
<WindowScroller
|
||||
scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
{({ height, isScrolling }) => {
|
||||
return (
|
||||
<WindowScroller
|
||||
scrollElement={isSmallScreen ? undefined : scroller}
|
||||
>
|
||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||
if (!height) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Measure
|
||||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<Scroller
|
||||
className={className}
|
||||
scrollDirection={scrollDirections.HORIZONTAL}
|
||||
>
|
||||
{header}
|
||||
|
||||
<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}
|
||||
/>
|
||||
<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>
|
||||
</Scroller>
|
||||
);
|
||||
}
|
||||
}
|
||||
</WindowScroller>
|
||||
</Measure>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
</WindowScroller>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VirtualTable.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
className: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
scrollIndex: PropTypes.number,
|
||||
contentBody: PropTypes.object.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
header: PropTypes.node.isRequired,
|
||||
headerHeight: PropTypes.number.isRequired,
|
||||
rowRenderer: PropTypes.func.isRequired,
|
||||
rowHeight: PropTypes.number.isRequired,
|
||||
onRender: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
rowHeight: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
VirtualTable.defaultProps = {
|
||||
className: styles.tableContainer,
|
||||
headerHeight: 38,
|
||||
onRender: () => {}
|
||||
headerHeight: 38
|
||||
};
|
||||
|
||||
export default VirtualTable;
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.tableBodyContainer {
|
||||
position: relative;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
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;
|
||||
@@ -8,6 +8,16 @@ 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: 12 KiB After Width: | Height: | Size: 3.7 KiB |
44
frontend/src/Diagnostic/Diagnostic.js
Normal file
44
frontend/src/Diagnostic/Diagnostic.js
Normal file
@@ -0,0 +1,44 @@
|
||||
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;
|
||||
82
frontend/src/Diagnostic/Script/MonacoEditor.js
Normal file
82
frontend/src/Diagnostic/Script/MonacoEditor.js
Normal file
@@ -0,0 +1,82 @@
|
||||
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;
|
||||
93
frontend/src/Diagnostic/Script/ScriptConnector.js
Normal file
93
frontend/src/Diagnostic/Script/ScriptConnector.js
Normal file
@@ -0,0 +1,93 @@
|
||||
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);
|
||||
6
frontend/src/Diagnostic/Script/ScriptConsole.css
Normal file
6
frontend/src/Diagnostic/Script/ScriptConsole.css
Normal file
@@ -0,0 +1,6 @@
|
||||
.split {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
139
frontend/src/Diagnostic/Script/ScriptConsole.js
Normal file
139
frontend/src/Diagnostic/Script/ScriptConsole.js
Normal file
@@ -0,0 +1,139 @@
|
||||
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;
|
||||
5
frontend/src/Diagnostic/Status/Statistics/Statistics.css
Normal file
5
frontend/src/Diagnostic/Status/Statistics/Statistics.css
Normal file
@@ -0,0 +1,5 @@
|
||||
.descriptionList {
|
||||
composes: descriptionList from '~Components/DescriptionList/DescriptionList.css';
|
||||
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
93
frontend/src/Diagnostic/Status/Statistics/Statistics.js
Normal file
93
frontend/src/Diagnostic/Status/Statistics/Statistics.js
Normal file
@@ -0,0 +1,93 @@
|
||||
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;
|
||||
46
frontend/src/Diagnostic/Status/Status.js
Normal file
46
frontend/src/Diagnostic/Status/Status.js
Normal file
@@ -0,0 +1,46 @@
|
||||
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;
|
||||
59
frontend/src/Diagnostic/Status/StatusConnector.js
Normal file
59
frontend/src/Diagnostic/Status/StatusConnector.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// @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);
|
||||
@@ -35,6 +35,10 @@ function EpisodeQuality(props) {
|
||||
isCutoffNotMet
|
||||
} = props;
|
||||
|
||||
if (!quality) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
className={className}
|
||||
|
||||
@@ -27,7 +27,7 @@ function EpisodeStatus(props) {
|
||||
size
|
||||
} = queueItem;
|
||||
|
||||
const progress = (100 - sizeleft / size * 100);
|
||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
|
||||
@@ -2,6 +2,7 @@ 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) {
|
||||
@@ -60,6 +61,10 @@ function SceneInfo(props) {
|
||||
key={alternateTitle.title}
|
||||
>
|
||||
{alternateTitle.title}
|
||||
{
|
||||
alternateTitle.sceneSeasonNumber !== -1 &&
|
||||
<span> (S{padNumber(alternateTitle.sceneSeasonNumber, 2)})</span>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -81,6 +81,7 @@ import {
|
||||
faSignOutAlt as fasSignOutAlt,
|
||||
faSitemap as fasSitemap,
|
||||
faSpinner as fasSpinner,
|
||||
faStepForward as fasStepForward,
|
||||
faSort as fasSort,
|
||||
faSortDown as fasSortDown,
|
||||
faSortUp as fasSortUp,
|
||||
@@ -126,6 +127,7 @@ 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;
|
||||
@@ -180,6 +182,8 @@ 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;
|
||||
|
||||
@@ -66,6 +66,7 @@ const columns = [
|
||||
{
|
||||
name: 'size',
|
||||
label: 'Size',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
|
||||
@@ -47,6 +47,7 @@ class InteractiveImportModal extends Component {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -62,7 +62,10 @@ class SelectSeriesModalContent extends Component {
|
||||
onChange={this.onFilterChange}
|
||||
/>
|
||||
|
||||
<Scroller className={styles.scroller}>
|
||||
<Scroller
|
||||
className={styles.scroller}
|
||||
autoFocus={false}
|
||||
>
|
||||
{
|
||||
items.map((item) => {
|
||||
return item.title.toLowerCase().includes(filter) ?
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
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>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -183,7 +183,7 @@ class SeasonPass extends Component {
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoSeries totalItems={totalItems} />
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
<SeasonPassFooter
|
||||
selectedCount={this.getSelectedIds().length}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
.episodeNumber {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 50px;
|
||||
width: 65px;
|
||||
}
|
||||
|
||||
.size {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -8,7 +9,8 @@ function SeasonInfo(props) {
|
||||
const {
|
||||
totalEpisodeCount,
|
||||
monitoredEpisodeCount,
|
||||
episodeFileCount
|
||||
episodeFileCount,
|
||||
sizeOnDisk
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -33,6 +35,13 @@ function SeasonInfo(props) {
|
||||
title="With Files"
|
||||
data={episodeFileCount}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
titleClassName={styles.title}
|
||||
descriptionClassName={styles.description}
|
||||
title="Size on Disk"
|
||||
data={formatBytes(sizeOnDisk)}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
@@ -40,7 +49,8 @@ function SeasonInfo(props) {
|
||||
SeasonInfo.propTypes = {
|
||||
totalEpisodeCount: PropTypes.number.isRequired,
|
||||
monitoredEpisodeCount: PropTypes.number.isRequired,
|
||||
episodeFileCount: PropTypes.number.isRequired
|
||||
episodeFileCount: PropTypes.number.isRequired,
|
||||
sizeOnDisk: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default SeasonInfo;
|
||||
|
||||
@@ -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 PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
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>
|
||||
|
||||
<PageContentBodyConnector innerClassName={styles.innerContentBody}>
|
||||
<PageContentBody innerClassName={styles.innerContentBody}>
|
||||
<div className={styles.header}>
|
||||
<div
|
||||
className={styles.backdrop}
|
||||
@@ -646,7 +646,7 @@ class SeriesDetails extends Component {
|
||||
showImportMode={false}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.sizeOnDisk {
|
||||
margin-left: 10px;
|
||||
color: #777;
|
||||
font-size: $defaultFontSize;
|
||||
}
|
||||
|
||||
.expandButton {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
@@ -37,7 +43,7 @@
|
||||
.left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 1 300px;
|
||||
flex: 0 1 350px;
|
||||
}
|
||||
|
||||
.left,
|
||||
@@ -115,4 +121,8 @@
|
||||
position: static;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sizeOnDisk {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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';
|
||||
@@ -33,6 +34,7 @@ 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))) {
|
||||
@@ -56,7 +58,8 @@ function getSeasonStatistics(episodes) {
|
||||
episodeFileCount,
|
||||
totalEpisodeCount,
|
||||
monitoredEpisodeCount,
|
||||
hasMonitoredEpisodes
|
||||
hasMonitoredEpisodes,
|
||||
sizeOnDisk
|
||||
};
|
||||
}
|
||||
|
||||
@@ -205,6 +208,7 @@ class SeriesDetailsSeason extends Component {
|
||||
seasonNumber,
|
||||
items,
|
||||
columns,
|
||||
statistics,
|
||||
isSaving,
|
||||
isExpanded,
|
||||
isSearching,
|
||||
@@ -271,11 +275,20 @@ 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
|
||||
@@ -504,6 +517,7 @@ 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,
|
||||
@@ -516,4 +530,8 @@ SeriesDetailsSeason.propTypes = {
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
SeriesDetailsSeason.defaultProps = {
|
||||
statistics: {}
|
||||
};
|
||||
|
||||
export default SeriesDetailsSeason;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -27,8 +26,8 @@ function createMapStateToProps() {
|
||||
seasonNumber
|
||||
}));
|
||||
|
||||
const episodesInSeason = _.filter(episodes.items, { seasonNumber });
|
||||
const sortedEpisodes = _.orderBy(episodesInSeason, 'episodeNumber', 'desc');
|
||||
const episodesInSeason = episodes.items.filter((episode) => episode.seasonNumber === seasonNumber);
|
||||
const sortedEpisodes = episodesInSeason.sort((a, b) => b.episodeNumber - a.episodeNumber);
|
||||
|
||||
return {
|
||||
items: sortedEpisodes,
|
||||
|
||||
@@ -3,13 +3,16 @@ 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, sortDirections } from 'Helpers/Props';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
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';
|
||||
@@ -18,58 +21,6 @@ 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 {
|
||||
|
||||
//
|
||||
@@ -83,8 +34,7 @@ class SeriesEditor extends Component {
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isOrganizingSeriesModalOpen: false,
|
||||
columns: getColumns(props.showLanguageProfile)
|
||||
isOrganizingSeriesModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
@@ -152,6 +102,7 @@ class SeriesEditor extends Component {
|
||||
error,
|
||||
totalItems,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
@@ -162,7 +113,7 @@ class SeriesEditor extends Component {
|
||||
isDeleting,
|
||||
deleteError,
|
||||
isOrganizingSeries,
|
||||
showLanguageProfile,
|
||||
onTableOptionChange,
|
||||
onSortPress,
|
||||
onFilterSelect
|
||||
} = this.props;
|
||||
@@ -170,8 +121,7 @@ class SeriesEditor extends Component {
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
columns
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const selectedSeriesIds = this.getSelectedIds();
|
||||
@@ -181,6 +131,18 @@ 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}
|
||||
@@ -192,7 +154,7 @@ class SeriesEditor extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -239,7 +201,7 @@ class SeriesEditor extends Component {
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoSeries totalItems={totalItems} />
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
</PageContentBody>
|
||||
|
||||
<SeriesEditorFooter
|
||||
seriesIds={selectedSeriesIds}
|
||||
@@ -249,7 +211,8 @@ class SeriesEditor extends Component {
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
isOrganizingSeries={isOrganizingSeries}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
columns={columns}
|
||||
showLanguageProfile={columns.find((column) => column.name === 'languageProfileId').isVisible}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
onOrganizeSeriesPress={this.onOrganizeSeriesPress}
|
||||
/>
|
||||
@@ -270,6 +233,7 @@ 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,
|
||||
@@ -280,7 +244,7 @@ SeriesEditor.propTypes = {
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
isOrganizingSeries: PropTypes.bool.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
|
||||
@@ -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, saveSeriesEditor } from 'Store/Actions/seriesEditorActions';
|
||||
import { setSeriesEditorSort, setSeriesEditorFilter, setSeriesEditorTableOption, saveSeriesEditor } from 'Store/Actions/seriesEditorActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
@@ -12,13 +12,11 @@ import SeriesEditor from './SeriesEditor';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.languageProfiles,
|
||||
createClientSideCollectionSelector('series', 'seriesEditor'),
|
||||
createCommandExecutingSelector(commandNames.RENAME_SERIES),
|
||||
(languageProfiles, series, isOrganizingSeries) => {
|
||||
(series, isOrganizingSeries) => {
|
||||
return {
|
||||
isOrganizingSeries,
|
||||
showLanguageProfile: languageProfiles.items.length > 1,
|
||||
...series
|
||||
};
|
||||
}
|
||||
@@ -28,6 +26,7 @@ function createMapStateToProps() {
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetSeriesEditorSort: setSeriesEditorSort,
|
||||
dispatchSetSeriesEditorFilter: setSeriesEditorFilter,
|
||||
dispatchSetSeriesEditorTableOption: setSeriesEditorTableOption,
|
||||
dispatchSaveSeriesEditor: saveSeriesEditor,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchExecuteCommand: executeCommand
|
||||
@@ -53,6 +52,10 @@ class SeriesEditorConnector extends Component {
|
||||
this.props.dispatchSetSeriesEditorFilter({ selectedFilterKey });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.dispatchSetSeriesEditorTableOption(payload);
|
||||
}
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.dispatchSaveSeriesEditor(payload);
|
||||
}
|
||||
@@ -74,6 +77,7 @@ class SeriesEditorConnector extends Component {
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -82,6 +86,7 @@ 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
|
||||
|
||||
@@ -145,7 +145,7 @@ class SeriesEditorFooter extends Component {
|
||||
isSaving,
|
||||
isDeleting,
|
||||
isOrganizingSeries,
|
||||
showLanguageProfile,
|
||||
columns,
|
||||
onOrganizeSeriesPress
|
||||
} = this.props;
|
||||
|
||||
@@ -192,85 +192,130 @@ 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>
|
||||
|
||||
{
|
||||
showLanguageProfile &&
|
||||
<div className={styles.inputContainer}>
|
||||
<SeriesEditorFooterLabel
|
||||
label="Language Profile"
|
||||
isSaving={isSaving && languageProfileId !== NO_CHANGE}
|
||||
/>
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
<LanguageProfileSelectInputConnector
|
||||
name="languageProfileId"
|
||||
value={languageProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
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>
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
<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
|
||||
@@ -346,6 +391,7 @@ 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
|
||||
};
|
||||
|
||||
@@ -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,6 +36,7 @@ class SeriesEditorRow extends Component {
|
||||
seriesType,
|
||||
seasonFolder,
|
||||
path,
|
||||
statistics,
|
||||
tags,
|
||||
columns,
|
||||
isSelected,
|
||||
@@ -50,51 +51,109 @@ 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>
|
||||
|
||||
{
|
||||
_.find(columns, { name: 'languageProfileId' }).isVisible &&
|
||||
<TableRowCell>
|
||||
{languageProfile.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>
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -111,6 +170,7 @@ 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,
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
$hoverScale: 1.05;
|
||||
|
||||
.container {
|
||||
&:hover {
|
||||
.content {
|
||||
background-color: $tableRowHoverBackgroundColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
@@ -70,7 +70,6 @@ class SeriesIndexOverview extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
style,
|
||||
id,
|
||||
title,
|
||||
overview,
|
||||
@@ -121,7 +120,7 @@ class SeriesIndexOverview extends Component {
|
||||
const overviewHeight = contentHeight - titleRowHeight;
|
||||
|
||||
return (
|
||||
<div className={styles.container} style={style}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.poster}>
|
||||
<div className={styles.posterContainer}>
|
||||
@@ -242,7 +241,6 @@ class SeriesIndexOverview extends Component {
|
||||
}
|
||||
|
||||
SeriesIndexOverview.propTypes = {
|
||||
style: PropTypes.object.isRequired,
|
||||
id: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string.isRequired,
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.grid {
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.container {
|
||||
&:hover {
|
||||
.content {
|
||||
background-color: $tableRowHoverBackgroundColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
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 hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
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';
|
||||
@@ -66,56 +63,44 @@ class SeriesIndexOverviews extends Component {
|
||||
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {})
|
||||
};
|
||||
|
||||
this._isInitialized = false;
|
||||
this._grid = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
items,
|
||||
filters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
overviewOptions,
|
||||
jumpToCharacter
|
||||
} = this.props;
|
||||
|
||||
const itemsChanged = hasDifferentItems(prevProps.items, items);
|
||||
const overviewOptionsChanged = !_.isMatch(prevProps.overviewOptions, overviewOptions);
|
||||
const {
|
||||
width,
|
||||
rowHeight
|
||||
} = this.state;
|
||||
|
||||
if (
|
||||
prevProps.sortKey !== sortKey ||
|
||||
prevProps.overviewOptions !== overviewOptions ||
|
||||
itemsChanged
|
||||
) {
|
||||
if (prevProps.sortKey !== sortKey ||
|
||||
prevProps.overviewOptions !== overviewOptions) {
|
||||
this.calculateGrid();
|
||||
}
|
||||
|
||||
if (
|
||||
prevProps.filters !== filters ||
|
||||
prevProps.sortKey !== sortKey ||
|
||||
prevProps.sortDirection !== sortDirection ||
|
||||
itemsChanged ||
|
||||
overviewOptionsChanged
|
||||
) {
|
||||
if (this._grid &&
|
||||
(prevState.width !== width ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items))) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (index != null) {
|
||||
const {
|
||||
rowHeight
|
||||
} = this.state;
|
||||
if (this._grid && index != null) {
|
||||
|
||||
const scrollTop = rowHeight * index;
|
||||
|
||||
this.props.onScroll({ scrollTop });
|
||||
this._grid.scrollToCell({
|
||||
rowIndex: index,
|
||||
columnIndex: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,21 +108,6 @@ 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;
|
||||
}
|
||||
@@ -185,24 +155,30 @@ class SeriesIndexOverviews extends Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<SeriesIndexItemConnector
|
||||
<div
|
||||
className={styles.container}
|
||||
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}
|
||||
seriesId={series.id}
|
||||
languageProfileId={series.languageProfileId}
|
||||
qualityProfileId={series.qualityProfileId}
|
||||
/>
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -213,22 +189,14 @@ 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,
|
||||
scrollTop,
|
||||
isSmallScreen,
|
||||
onScroll
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -237,28 +205,38 @@ class SeriesIndexOverviews extends Component {
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Measure onMeasure={this.onMeasure}>
|
||||
<Measure
|
||||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<WindowScroller
|
||||
scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
|
||||
onScroll={onScroll}
|
||||
scrollElement={isSmallScreen ? undefined : scroller}
|
||||
>
|
||||
{({ height, isScrolling }) => {
|
||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||
if (!height) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<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}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -270,20 +248,15 @@ 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,
|
||||
contentBody: PropTypes.object.isRequired,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onRender: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default SeriesIndexOverviews;
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
$hoverScale: 1.05;
|
||||
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.content {
|
||||
transition: all 200ms ease-in;
|
||||
|
||||
|
||||
@@ -67,7 +67,6 @@ class SeriesIndexPoster extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
style,
|
||||
id,
|
||||
title,
|
||||
monitored,
|
||||
@@ -115,147 +114,144 @@ class SeriesIndexPoster extends Component {
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
<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}
|
||||
/>
|
||||
|
||||
{
|
||||
status === 'ended' &&
|
||||
<div
|
||||
className={styles.ended}
|
||||
title="Ended"
|
||||
showSearchAction &&
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
name={icons.SEARCH}
|
||||
title="Search for monitored episodes"
|
||||
isSpinning={isSearchingSeries}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
}
|
||||
|
||||
<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}
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.EDIT}
|
||||
title="Edit Series"
|
||||
onPress={this.onEditSeriesPress}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{
|
||||
status === 'ended' &&
|
||||
<div
|
||||
className={styles.ended}
|
||||
title="Ended"
|
||||
/>
|
||||
|
||||
{
|
||||
hasPosterError &&
|
||||
<div className={styles.overlayTitle}>
|
||||
{title}
|
||||
</div>
|
||||
}
|
||||
</Link>
|
||||
</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>
|
||||
}
|
||||
<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}
|
||||
/>
|
||||
|
||||
{
|
||||
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}
|
||||
/>
|
||||
{
|
||||
hasPosterError &&
|
||||
<div className={styles.overlayTitle}>
|
||||
{title}
|
||||
</div>
|
||||
}
|
||||
</Link>
|
||||
</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
Reference in New Issue
Block a user