mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Many UI Updates and Performance Tweaks
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
.page {
|
||||
composes: page from './Page.css';
|
||||
composes: page from '~./Page.css';
|
||||
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@@ -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;
|
||||
|
@@ -38,7 +38,7 @@
|
||||
}
|
||||
|
||||
.donate {
|
||||
composes: link from 'Components/Link/Link.css';
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
width: 30px;
|
||||
color: $themeRed;
|
||||
|
@@ -1,3 +1,3 @@
|
||||
.page {
|
||||
composes: page from './Page.css';
|
||||
composes: page from '~./Page.css';
|
||||
}
|
||||
|
@@ -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
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@@ -1,3 +1,3 @@
|
||||
.content {
|
||||
composes: content from './PageContent.css';
|
||||
composes: content from '~./PageContent.css';
|
||||
}
|
||||
|
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -87,6 +87,10 @@ const links = [
|
||||
title: 'Quality',
|
||||
to: '/settings/quality'
|
||||
},
|
||||
{
|
||||
title: 'Custom Formats',
|
||||
to: '/settings/customformats'
|
||||
},
|
||||
{
|
||||
title: 'Indexers',
|
||||
to: '/settings/indexers'
|
||||
|
@@ -1,3 +1,3 @@
|
||||
.status {
|
||||
composes: label from 'Components/Label.css';
|
||||
composes: label from '~Components/Label.css';
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user