New: Many UI Updates and Performance Tweaks

This commit is contained in:
Qstick
2019-04-12 23:25:58 -04:00
parent b24a40797f
commit 6275737ced
389 changed files with 7961 additions and 5635 deletions

View File

@@ -1,5 +1,5 @@
.page {
composes: page from './Page.css';
composes: page from '~./Page.css';
margin-top: 20px;
text-align: center;

View File

@@ -11,7 +11,8 @@ function ErrorPage(props) {
customFiltersError,
tagsError,
qualityProfilesError,
uiSettingsError
uiSettingsError,
systemStatusError
} = props;
let errorMessage = 'Failed to load Radarr';
@@ -28,6 +29,8 @@ function ErrorPage(props) {
errorMessage = getErrorMessage(qualityProfilesError, 'Failed to load quality profiles from API');
} else if (uiSettingsError) {
errorMessage = getErrorMessage(uiSettingsError, 'Failed to load UI settings from API');
} else if (systemStatusError) {
errorMessage = getErrorMessage(uiSettingsError, 'Failed to load system status from API');
}
return (
@@ -50,7 +53,8 @@ ErrorPage.propTypes = {
customFiltersError: PropTypes.object,
tagsError: PropTypes.object,
qualityProfilesError: PropTypes.object,
uiSettingsError: PropTypes.object
uiSettingsError: PropTypes.object,
systemStatusError: PropTypes.object
};
export default ErrorPage;

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
import jdu from 'jdu';
import Fuse from 'fuse.js';
import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon';
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
@@ -10,6 +10,21 @@ import styles from './MovieSearchInput.css';
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'
]
};
class MovieSearchInput extends Component {
//
@@ -69,16 +84,15 @@ class MovieSearchInput extends Component {
return (
<MovieSearchResult
query={query}
cleanQuery={jdu.replace(query).toLowerCase()}
{...item}
{...item.item}
match={item.matches[0]}
/>
);
}
goToMovie(movie) {
goToMovie(item) {
this.setState({ value: '' });
this.props.onGoToMovie(movie.titleSlug);
this.props.onGoToMovie(item.item.titleSlug);
}
reset() {
@@ -140,26 +154,8 @@ class MovieSearchInput extends Component {
}
onSuggestionsFetchRequested = ({ value }) => {
const lowerCaseValue = jdu.replace(value).toLowerCase();
const suggestions = this.props.movie.filter((movie) => {
// Check the title first and if there isn't a match fallback to
// the alternate titles and finally the tags.
if (value.length === 1) {
return (
movie.cleanTitle.startsWith(lowerCaseValue) ||
movie.alternateTitles.some((alternateTitle) => alternateTitle.cleanTitle.startsWith(lowerCaseValue)) ||
movie.tags.some((tag) => tag.cleanLabel.startsWith(lowerCaseValue))
);
}
return (
movie.cleanTitle.contains(lowerCaseValue) ||
movie.alternateTitles.some((alternateTitle) => alternateTitle.cleanTitle.contains(lowerCaseValue)) ||
movie.tags.some((tag) => tag.cleanLabel.contains(lowerCaseValue))
);
});
const fuse = new Fuse(this.props.movies, fuseOptions);
const suggestions = fuse.search(value);
this.setState({ suggestions });
}
@@ -209,7 +205,7 @@ class MovieSearchInput extends Component {
const inputProps = {
ref: this.setInputRef,
className: styles.input,
name: 'seriesSearch',
name: 'movieSearch',
value,
placeholder: 'Search',
autoComplete: 'off',
@@ -255,7 +251,7 @@ class MovieSearchInput extends Component {
}
MovieSearchInput.propTypes = {
movie: PropTypes.arrayOf(PropTypes.object).isRequired,
movies: PropTypes.arrayOf(PropTypes.object).isRequired,
onGoToMovie: PropTypes.func.isRequired,
onGoToAddNewMovie: PropTypes.func.isRequired,
bindShortcut: PropTypes.func.isRequired

View File

@@ -1,35 +1,14 @@
import { connect } from 'react-redux';
import { push } from 'react-router-redux';
import { push } from 'connected-react-router';
import { createSelector } from 'reselect';
import jdu from 'jdu';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import MovieSearchInput from './MovieSearchInput';
function createCleanTagsSelector() {
return createSelector(
createTagsSelector(),
(tags) => {
return tags.map((tag) => {
const {
id,
label
} = tag;
return {
id,
label,
cleanLabel: jdu.replace(label).toLowerCase()
};
});
}
);
}
function createCleanMovieSelector() {
return createSelector(
createAllMoviesSelector(),
createCleanTagsSelector(),
createTagsSelector(),
(allMovies, allTags) => {
return allMovies.map((movie) => {
const {
@@ -46,27 +25,11 @@ function createCleanMovieSelector() {
titleSlug,
sortTitle,
images,
cleanTitle: jdu.replace(title).toLowerCase(),
alternateTitles: alternateTitles.map((alternateTitle) => {
return {
title: alternateTitle.title,
sortTitle: alternateTitle.sortTitle,
cleanTitle: jdu.replace(alternateTitle.title).toLowerCase()
};
}),
alternateTitles,
tags: tags.map((id) => {
return allTags.find((tag) => tag.id === id);
})
};
}).sort((a, b) => {
if (a.sortTitle < b.sortTitle) {
return -1;
}
if (a.sortTitle > b.sortTitle) {
return 1;
}
return 0;
});
}
);
@@ -75,9 +38,9 @@ function createCleanMovieSelector() {
function createMapStateToProps() {
return createSelector(
createCleanMovieSelector(),
(movie) => {
(movies) => {
return {
movie
movies
};
}
);

View File

@@ -5,38 +5,22 @@ import Label from 'Components/Label';
import MoviePoster from 'Movie/MoviePoster';
import styles from './MovieSearchResult.css';
function findMatchingAlternateTitle(alternateTitles, cleanQuery) {
return alternateTitles.find((alternateTitle) => {
return alternateTitle.cleanTitle.contains(cleanQuery);
});
}
function getMatchingTag(tags, cleanQuery) {
return tags.find((tag) => {
return tag.cleanLabel.contains(cleanQuery);
});
}
function MovieSearchResult(props) {
const {
cleanQuery,
match,
title,
cleanTitle,
images,
alternateTitles,
tags
} = props;
const titleContains = cleanTitle.contains(cleanQuery);
let alternateTitle = null;
let tag = null;
if (!titleContains) {
alternateTitle = findMatchingAlternateTitle(alternateTitles, cleanQuery);
}
if (!titleContains && !alternateTitle) {
tag = getMatchingTag(tags, cleanQuery);
if (match.key === 'alternateTitles.cleanTitle') {
alternateTitle = alternateTitles[match.arrayIndex];
} else if (match.key === 'tags.label') {
tag = tags[match.arrayIndex];
}
return (
@@ -55,14 +39,15 @@ function MovieSearchResult(props) {
</div>
{
!!alternateTitle &&
alternateTitle ?
<div className={styles.alternateTitle}>
{alternateTitle.title}
</div>
</div> :
null
}
{
!!tag &&
tag ?
<div className={styles.tagContainer}>
<Label
key={tag.id}
@@ -70,7 +55,8 @@ function MovieSearchResult(props) {
>
{tag.label}
</Label>
</div>
</div> :
null
}
</div>
</div>
@@ -78,12 +64,11 @@ function MovieSearchResult(props) {
}
MovieSearchResult.propTypes = {
cleanQuery: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
cleanTitle: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
tags: PropTypes.arrayOf(PropTypes.object).isRequired
tags: PropTypes.arrayOf(PropTypes.object).isRequired,
match: PropTypes.object.isRequired
};
export default MovieSearchResult;

View File

@@ -38,7 +38,7 @@
}
.donate {
composes: link from 'Components/Link/Link.css';
composes: link from '~Components/Link/Link.css';
width: 30px;
color: $themeRed;

View File

@@ -1,3 +1,3 @@
.page {
composes: page from './Page.css';
composes: page from '~./Page.css';
}

View File

@@ -27,54 +27,103 @@ function testLocalStorage() {
}
}
const selectAppProps = createSelector(
(state) => state.app.isSidebarVisible,
(state) => state.app.version,
(state) => state.app.isUpdated,
(state) => state.app.isDisconnected,
(isSidebarVisible, version, isUpdated, isDisconnected) => {
return {
isSidebarVisible,
version,
isUpdated,
isDisconnected
};
}
);
const selectIsPopulated = createSelector(
(state) => state.movies.isPopulated,
(state) => state.customFilters.isPopulated,
(state) => state.tags.isPopulated,
(state) => state.settings.ui.isPopulated,
(state) => state.settings.qualityProfiles.isPopulated,
(state) => state.system.status.isPopulated,
(
moviesIsPopulated,
customFiltersIsPopulated,
tagsIsPopulated,
uiSettingsIsPopulated,
qualityProfilesIsPopulated,
systemStatusIsPopulated
) => {
return (
moviesIsPopulated &&
customFiltersIsPopulated &&
tagsIsPopulated &&
uiSettingsIsPopulated &&
qualityProfilesIsPopulated &&
systemStatusIsPopulated
);
}
);
const selectErrors = createSelector(
(state) => state.movies.error,
(state) => state.customFilters.error,
(state) => state.tags.error,
(state) => state.settings.ui.error,
(state) => state.settings.qualityProfiles.error,
(state) => state.system.status.error,
(
moviesError,
customFiltersError,
tagsError,
uiSettingsError,
qualityProfilesError,
systemStatusError
) => {
const hasError = !!(
moviesError ||
customFiltersError ||
tagsError ||
uiSettingsError ||
qualityProfilesError ||
systemStatusError
);
return {
hasError,
moviesError,
customFiltersError,
tagsError,
uiSettingsError,
qualityProfilesError,
systemStatusError
};
}
);
function createMapStateToProps() {
return createSelector(
(state) => state.movies,
(state) => state.customFilters,
(state) => state.tags,
(state) => state.settings.ui,
(state) => state.settings.qualityProfiles,
(state) => state.app,
(state) => state.settings.ui.item.enableColorImpairedMode,
selectIsPopulated,
selectErrors,
selectAppProps,
createDimensionsSelector(),
(
movies,
customFilters,
tags,
uiSettings,
qualityProfiles,
enableColorImpairedMode,
isPopulated,
errors,
app,
dimensions
) => {
const isPopulated = (
movies.isPopulated &&
customFilters.isPopulated &&
tags.isPopulated &&
qualityProfiles.isPopulated &&
uiSettings.isPopulated
);
const hasError = !!(
movies.error ||
customFilters.error ||
tags.error ||
qualityProfiles.error ||
uiSettings.error
);
return {
...app,
...errors,
isPopulated,
hasError,
moviesError: movies.error,
customFiltersError: tags.error,
tagsError: tags.error,
qualityProfilesError: qualityProfiles.error,
uiSettingsError: uiSettings.error,
isSmallScreen: dimensions.isSmallScreen,
isSidebarVisible: app.isSidebarVisible,
enableColorImpairedMode: uiSettings.item.enableColorImpairedMode,
version: app.version,
isUpdated: app.isUpdated,
isDisconnected: app.isDisconnected
enableColorImpairedMode
};
}
);

View File

@@ -1,3 +1,3 @@
.content {
composes: content from './PageContent.css';
composes: content from '~./PageContent.css';
}

View File

@@ -29,7 +29,8 @@ class PageJumpBar extends Component {
shouldComponentUpdate(nextProps, nextState) {
return (
nextProps.items !== this.props.items ||
nextState.height !== this.state.height
nextState.height !== this.state.height ||
nextState.visibleItems !== this.state.visibleItems
);
}

View File

@@ -87,6 +87,10 @@ const links = [
title: 'Quality',
to: '/settings/quality'
},
{
title: 'Custom Formats',
to: '/settings/customformats'
},
{
title: 'Indexers',
to: '/settings/indexers'

View File

@@ -1,3 +1,3 @@
.status {
composes: label from 'Components/Label.css';
composes: label from '~Components/Label.css';
}

View File

@@ -1,6 +1,7 @@
.toolbarButton {
composes: link from 'Components/Link/Link.css';
composes: link from '~Components/Link/Link.css';
padding-top: 4px;
width: $toolbarButtonWidth;
text-align: center;
@@ -21,7 +22,7 @@
display: flex;
align-items: center;
justify-content: center;
min-height: 16px;
height: 24px;
}
.label {

View File

@@ -6,7 +6,7 @@
.section {
display: flex;
align-items: center;
align-items: stretch;
flex-grow: 1;
}
@@ -23,7 +23,7 @@
}
.overflowMenuButton {
composes: menuButton from 'Components/Menu/ToolbarMenuButton.css';
composes: menuButton from '~Components/Menu/ToolbarMenuButton.css';
}
.overflowMenuItemIcon {