mirror of https://github.com/lidarr/Lidarr
New: Server Side UI Filtering, Error Boundaries (#501)
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
parent
a95191dc3b
commit
64a8d02f77
|
@ -1,4 +1,4 @@
|
|||
// will download and run sonarr (server) in a non-windows enviroment
|
||||
// will download and run lidarr (server) in a non-windows enviroment
|
||||
// you can use this if you don't care about the server code and just want to work
|
||||
// with the web code.
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
|||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
||||
import TrackQuality from 'Album/TrackQuality';
|
||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||
import BlacklistDetailsModal from './BlacklistDetailsModal';
|
||||
import styles from './BlacklistRow.css';
|
||||
|
@ -103,7 +103,7 @@ class BlacklistRow extends Component {
|
|||
key={name}
|
||||
className={styles.quality}
|
||||
>
|
||||
<EpisodeQuality
|
||||
<TrackQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
|
|
@ -7,7 +7,7 @@ import TableRow from 'Components/Table/TableRow';
|
|||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
||||
import TrackQuality from 'Album/TrackQuality';
|
||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||
|
@ -142,7 +142,7 @@ class HistoryRow extends Component {
|
|||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<EpisodeQuality
|
||||
<TrackQuality
|
||||
quality={quality}
|
||||
isCutoffMet={qualityCutoffNotMet}
|
||||
/>
|
||||
|
|
|
@ -10,7 +10,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
||||
import TrackQuality from 'Album/TrackQuality';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||
import QueueStatusCell from './QueueStatusCell';
|
||||
|
@ -177,7 +177,7 @@ class QueueRow extends Component {
|
|||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<EpisodeQuality
|
||||
<TrackQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { icons } from 'Helpers/Props';
|
|||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import InteractiveAlbumSearchModal from './Search/InteractiveAlbumSearchModal';
|
||||
import InteractiveSearchModal from 'InteractiveSearch/InteractiveSearchModal';
|
||||
import styles from './AlbumSearchCell.css';
|
||||
|
||||
class AlbumSearchCell extends Component {
|
||||
|
@ -55,7 +55,7 @@ class AlbumSearchCell extends Component {
|
|||
onPress={this.onManualSearchPress}
|
||||
/>
|
||||
|
||||
<InteractiveAlbumSearchModal
|
||||
<InteractiveSearchModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
albumId={albumId}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
|
|
|
@ -22,7 +22,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
|||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import AlbumDetailsMediumConnector from './AlbumDetailsMediumConnector';
|
||||
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
|
||||
import InteractiveAlbumSearchModal from 'Album/Search/InteractiveAlbumSearchModal';
|
||||
import InteractiveSearchModal from 'InteractiveSearch/InteractiveSearchModal';
|
||||
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
|
||||
|
||||
import styles from './AlbumDetails.css';
|
||||
|
@ -415,7 +415,7 @@ class AlbumDetails extends Component {
|
|||
onModalClose={this.onManageTracksModalClose}
|
||||
/>
|
||||
|
||||
<InteractiveAlbumSearchModal
|
||||
<InteractiveSearchModal
|
||||
isOpen={isInteractiveSearchModalOpen}
|
||||
albumId={id}
|
||||
onModalClose={this.onInteractiveSearchModalClose}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { icons, kinds, sizes } from 'Helpers/Props';
|
|||
import Icon from 'Components/Icon';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import QueueDetails from 'Activity/Queue/QueueDetails';
|
||||
import EpisodeQuality from './EpisodeQuality';
|
||||
import TrackQuality from './TrackQuality';
|
||||
import styles from './EpisodeStatus.css';
|
||||
|
||||
function EpisodeStatus(props) {
|
||||
|
@ -63,7 +63,7 @@ function EpisodeStatus(props) {
|
|||
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<EpisodeQuality
|
||||
<TrackQuality
|
||||
quality={quality}
|
||||
size={trackFile.size}
|
||||
isCutoffNotMet={isCutoffNotMet}
|
||||
|
|
|
@ -22,7 +22,7 @@ function getTooltip(title, quality, size) {
|
|||
return title;
|
||||
}
|
||||
|
||||
function EpisodeQuality(props) {
|
||||
function TrackQuality(props) {
|
||||
const {
|
||||
className,
|
||||
title,
|
||||
|
@ -42,7 +42,7 @@ function EpisodeQuality(props) {
|
|||
);
|
||||
}
|
||||
|
||||
EpisodeQuality.propTypes = {
|
||||
TrackQuality.propTypes = {
|
||||
className: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
quality: PropTypes.object.isRequired,
|
||||
|
@ -50,8 +50,8 @@ EpisodeQuality.propTypes = {
|
|||
isCutoffNotMet: PropTypes.bool
|
||||
};
|
||||
|
||||
EpisodeQuality.defaultProps = {
|
||||
TrackQuality.defaultProps = {
|
||||
title: ''
|
||||
};
|
||||
|
||||
export default EpisodeQuality;
|
||||
export default TrackQuality;
|
|
@ -13,6 +13,7 @@ import FilterMenu from 'Components/Menu/FilterMenu';
|
|||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import NoArtist from 'Artist/NoArtist';
|
||||
import AlbumStudioFilterModalConnector from './AlbumStudioFilterModalConnector';
|
||||
import AlbumStudioRowConnector from './AlbumStudioRowConnector';
|
||||
import AlbumStudioFooter from './AlbumStudioFooter';
|
||||
|
||||
|
@ -130,6 +131,7 @@ class AlbumStudio extends Component {
|
|||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={AlbumStudioFilterModalConnector}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
@ -202,7 +204,7 @@ AlbumStudio.propTypes = {
|
|||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setAlbumStudioFilter } from 'Store/Actions/albumStudioActions';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.artist.items,
|
||||
(state) => state.albumStudio.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'albumStudio'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetFilter: setAlbumStudioFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
|
@ -9,7 +9,7 @@ function createMapStateToProps() {
|
|||
return createSelector(
|
||||
createArtistSelector(),
|
||||
createTrackFileSelector(),
|
||||
(artist, trackFile) => {
|
||||
(artist = {}, trackFile) => {
|
||||
return {
|
||||
foreignArtistId: artist.foreignArtistId,
|
||||
artistMonitored: artist.monitored,
|
||||
|
|
|
@ -11,6 +11,7 @@ import HeartRating from 'Components/HeartRating';
|
|||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Label from 'Components/Label';
|
||||
import Measure from 'Components/Measure';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
|
|
|
@ -273,7 +273,7 @@ ArtistEditor.propTypes = {
|
|||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as artistEditorActions from 'Store/Actions/artistEditorActions';
|
||||
import { setArtistEditorFilter } from 'Store/Actions/artistEditorActions';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
@ -10,22 +10,15 @@ function createMapStateToProps() {
|
|||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps
|
||||
filterBuilderProps,
|
||||
customFilterType: 'artistEditor'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRemoveCustomFilterPress(payload) {
|
||||
dispatch(artistEditorActions.removeArtistEditorCustomFilter(payload));
|
||||
},
|
||||
|
||||
onSaveCustomFilterPress(payload) {
|
||||
dispatch(artistEditorActions.saveArtistEditorCustomFilter(payload));
|
||||
}
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetFilter: setArtistEditorFilter
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
||||
|
|
|
@ -119,4 +119,8 @@ ArtistEditorRow.propTypes = {
|
|||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ArtistEditorRow.defaultProps = {
|
||||
tags: []
|
||||
};
|
||||
|
||||
export default ArtistEditorRow;
|
||||
|
|
|
@ -9,7 +9,7 @@ import TableRow from 'Components/Table/TableRow';
|
|||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
||||
import TrackQuality from 'Album/TrackQuality';
|
||||
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||
import styles from './ArtistHistoryRow.css';
|
||||
|
@ -100,7 +100,7 @@ class ArtistHistoryRow extends Component {
|
|||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeQuality
|
||||
<TrackQuality
|
||||
quality={quality}
|
||||
isCutoffNotMet={qualityCutoffNotMet}
|
||||
/>
|
||||
|
|
|
@ -215,7 +215,7 @@ class ArtistIndex extends Component {
|
|||
} = this.state;
|
||||
|
||||
const ViewComponent = getViewComponent(view);
|
||||
const isLoaded = !error && isPopulated && !!items.length && contentBody;
|
||||
const isLoaded = !!(!error && isPopulated && items.length && contentBody);
|
||||
const hasNoArtist = !totalItems;
|
||||
|
||||
return (
|
||||
|
@ -382,7 +382,7 @@ ArtistIndex.propTypes = {
|
|||
error: PropTypes.object,
|
||||
totalItems: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as artistIndexActions from 'Store/Actions/artistIndexActions';
|
||||
import { setArtistFilter } from 'Store/Actions/artistIndexActions';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
@ -10,22 +10,15 @@ function createMapStateToProps() {
|
|||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps
|
||||
filterBuilderProps,
|
||||
customFilterType: 'artistIndex'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRemoveCustomFilterPress(payload) {
|
||||
dispatch(artistIndexActions.removeArtistCustomFilter(payload));
|
||||
},
|
||||
|
||||
onSaveCustomFilterPress(payload) {
|
||||
dispatch(artistIndexActions.saveArtistCustomFilter(payload));
|
||||
}
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetFilter: setArtistFilter
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Measure from 'react-measure';
|
||||
import { Grid, WindowScroller } from 'react-virtualized';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import Measure from 'Components/Measure';
|
||||
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
||||
import ArtistIndexBanner from './ArtistIndexBanner';
|
||||
import styles from './ArtistIndexBanners.css';
|
||||
|
|
|
@ -27,7 +27,7 @@ function ArtistIndexFilterMenu(props) {
|
|||
}
|
||||
|
||||
ArtistIndexFilterMenu.propTypes = {
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -2,12 +2,12 @@ import _ from 'lodash';
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Measure from 'react-measure';
|
||||
import { Grid, WindowScroller } from 'react-virtualized';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import Measure from 'Components/Measure';
|
||||
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
||||
import ArtistIndexOverview from './ArtistIndexOverview';
|
||||
import styles from './ArtistIndexOverviews.css';
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Measure from 'react-measure';
|
||||
import { Grid, WindowScroller } from 'react-virtualized';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import Measure from 'Components/Measure';
|
||||
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
||||
import ArtistIndexPoster from './ArtistIndexPoster';
|
||||
import styles from './ArtistIndexPosters.css';
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Measure from 'react-measure';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import Measure from 'Components/Measure';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
|
|
|
@ -56,6 +56,10 @@ class CalendarEvent extends Component {
|
|||
colorImpairedMode
|
||||
} = this.props;
|
||||
|
||||
if (!artist) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const startTime = moment(releaseDate);
|
||||
// const endTime = startTime.add(artist.runtime, 'minutes');
|
||||
const downloading = !!(queueItem || grabbed);
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import * as sentry from '@sentry/browser';
|
||||
|
||||
class ErrorBoundary extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
info: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
this.setState({
|
||||
error,
|
||||
info
|
||||
});
|
||||
|
||||
sentry.captureException(error);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
errorComponent: ErrorComponent,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
error,
|
||||
info
|
||||
} = this.state;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorComponent
|
||||
error={error}
|
||||
info={info}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
ErrorBoundary.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
errorComponent: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ErrorBoundary;
|
|
@ -0,0 +1,38 @@
|
|||
.container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 50px 0;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.image {
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin: 20px;
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.image {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.image {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import styles from './ErrorBoundaryError.css';
|
||||
|
||||
function ErrorBoundaryError(props) {
|
||||
const {
|
||||
className,
|
||||
messageClassName,
|
||||
detailsClassName,
|
||||
message,
|
||||
error,
|
||||
info
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={messageClassName}>
|
||||
{message}
|
||||
</div>
|
||||
|
||||
<div className={styles.imageContainer}>
|
||||
<img
|
||||
className={styles.image}
|
||||
src={`${window.Lidarr.urlBase}/Content/Images/error.png`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<details className={detailsClassName}>
|
||||
{
|
||||
error &&
|
||||
<div>
|
||||
{error.toString()}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.info}>
|
||||
{info.componentStack}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorBoundaryError.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
messageClassName: PropTypes.string.isRequired,
|
||||
detailsClassName: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
error: PropTypes.object.isRequired,
|
||||
info: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
ErrorBoundaryError.defaultProps = {
|
||||
className: styles.container,
|
||||
messageClassName: styles.message,
|
||||
detailsClassName: styles.details,
|
||||
message: 'There was an error loading this content'
|
||||
};
|
||||
|
||||
export default ErrorBoundaryError;
|
|
@ -35,7 +35,7 @@ function createMapStateToProps() {
|
|||
directories,
|
||||
files,
|
||||
paths: filteredPaths,
|
||||
isWindowsService: true || systemStatus.isWindows && systemStatus.mode === 'service'
|
||||
isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service'
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
|||
import { inputTypes } from 'Helpers/Props';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
|
@ -34,6 +35,28 @@ class FilterBuilderModalContent extends Component {
|
|||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
id,
|
||||
customFilters,
|
||||
isSaving,
|
||||
saveError,
|
||||
dispatchSetFilter,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
if (id) {
|
||||
dispatchSetFilter({ selectedFilterKey: id });
|
||||
} else {
|
||||
const last = customFilters[customFilters.length -1];
|
||||
dispatchSetFilter({ selectedFilterKey: last.id });
|
||||
}
|
||||
|
||||
onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
|
@ -70,9 +93,9 @@ class FilterBuilderModalContent extends Component {
|
|||
|
||||
onSaveFilterPress = () => {
|
||||
const {
|
||||
customFilterKey: key,
|
||||
onSaveCustomFilterPress,
|
||||
onModalClose
|
||||
id,
|
||||
customFilterType,
|
||||
onSaveCustomFilterPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
|
@ -92,8 +115,12 @@ class FilterBuilderModalContent extends Component {
|
|||
return;
|
||||
}
|
||||
|
||||
onSaveCustomFilterPress({ key, label, filters });
|
||||
onModalClose();
|
||||
onSaveCustomFilterPress({
|
||||
id,
|
||||
type: customFilterType,
|
||||
label,
|
||||
filters
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -103,6 +130,8 @@ class FilterBuilderModalContent extends Component {
|
|||
const {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
isSaving,
|
||||
saveError,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
|
@ -161,17 +190,17 @@ class FilterBuilderModalContent extends Component {
|
|||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isSaving}
|
||||
error={saveError}
|
||||
onPress={this.onSaveFilterPress}
|
||||
>
|
||||
Apply
|
||||
</Button>
|
||||
Save
|
||||
</SpinnerErrorButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
|
@ -179,13 +208,18 @@ class FilterBuilderModalContent extends Component {
|
|||
}
|
||||
|
||||
FilterBuilderModalContent.propTypes = {
|
||||
customFilterKey: PropTypes.string,
|
||||
id: PropTypes.number,
|
||||
label: PropTypes.string.isRequired,
|
||||
customFilterType: PropTypes.string.isRequired,
|
||||
sectionItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
filterBuilderProps: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onRemoveCustomFilterPress: PropTypes.func.isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
dispatchDeleteCustomFilter: PropTypes.func.isRequired,
|
||||
onSaveCustomFilterPress: PropTypes.func.isRequired,
|
||||
dispatchSetFilter: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -1,28 +1,42 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveCustomFilter, deleteCustomFilter } from 'Store/Actions/customFilterActions';
|
||||
import FilterBuilderModalContent from './FilterBuilderModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { customFilters }) => customFilters,
|
||||
(state, { customFilterKey }) => customFilterKey,
|
||||
(customFilters, customFilterKey) => {
|
||||
if (customFilterKey) {
|
||||
const customFilter = customFilters.find((c) => c.key === customFilterKey);
|
||||
(state, { id }) => id,
|
||||
(state) => state.customFilters.isSaving,
|
||||
(state) => state.customFilters.saveError,
|
||||
(customFilters, id, isSaving, saveError) => {
|
||||
if (id) {
|
||||
const customFilter = customFilters.find((c) => c.id === id);
|
||||
|
||||
return {
|
||||
customFilterKey: customFilter.key,
|
||||
id: customFilter.id,
|
||||
label: customFilter.label,
|
||||
filters: customFilter.filters
|
||||
filters: customFilter.filters,
|
||||
customFilters,
|
||||
isSaving,
|
||||
saveError
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: '',
|
||||
filters: []
|
||||
filters: [],
|
||||
customFilters,
|
||||
isSaving,
|
||||
saveError
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderModalContent);
|
||||
const mapDispatchToProps = {
|
||||
onSaveCustomFilterPress: saveCustomFilter,
|
||||
dispatchDeleteCustomFilter: deleteCustomFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterBuilderModalContent);
|
||||
|
|
|
@ -1,11 +1,64 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { kinds, filterBuilderTypes } from 'Helpers/Props';
|
||||
import convertToBytes from 'Utilities/Number/convertToBytes';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import { kinds, filterBuilderTypes, filterBuilderValueTypes } from 'Helpers/Props';
|
||||
import TagInput, { tagShape } from 'Components/Form/TagInput';
|
||||
import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
|
||||
|
||||
export const NAME = 'value';
|
||||
|
||||
function getTagDisplayValue(value, selectedFilterBuilderProp) {
|
||||
if (selectedFilterBuilderProp.valueType === filterBuilderValueTypes.BYTES) {
|
||||
return formatBytes(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function getValue(input, selectedFilterBuilderProp) {
|
||||
if (selectedFilterBuilderProp.valueType === filterBuilderValueTypes.BYTES) {
|
||||
const match = input.match(/^(\d+)([kmgt](i?b)?)$/i);
|
||||
if (match && match.length > 1) {
|
||||
const [, value, unit] = input.match(/^(\d+)([kmgt](i?b)?)$/i);
|
||||
switch (unit.toLowerCase()) {
|
||||
case 'k':
|
||||
return convertToBytes(value, 1, true);
|
||||
case 'm':
|
||||
return convertToBytes(value, 2, true);
|
||||
case 'g':
|
||||
return convertToBytes(value, 3, true);
|
||||
case 't':
|
||||
return convertToBytes(value, 4, true);
|
||||
case 'kb':
|
||||
return convertToBytes(value, 1, true);
|
||||
case 'mb':
|
||||
return convertToBytes(value, 2, true);
|
||||
case 'gb':
|
||||
return convertToBytes(value, 3, true);
|
||||
case 'tb':
|
||||
return convertToBytes(value, 4, true);
|
||||
case 'kib':
|
||||
return convertToBytes(value, 1, true);
|
||||
case 'mib':
|
||||
return convertToBytes(value, 2, true);
|
||||
case 'gib':
|
||||
return convertToBytes(value, 3, true);
|
||||
case 'tib':
|
||||
return convertToBytes(value, 4, true);
|
||||
default:
|
||||
return parseInt(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
|
||||
return parseInt(input);
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
class FilterBuilderRowValue extends Component {
|
||||
|
||||
//
|
||||
|
@ -18,17 +71,15 @@ class FilterBuilderRowValue extends Component {
|
|||
onChange
|
||||
} = this.props;
|
||||
|
||||
let id = tag.id;
|
||||
let value = tag.id;
|
||||
|
||||
if (id == null) {
|
||||
id = selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ?
|
||||
parseInt(tag.name) :
|
||||
tag.name;
|
||||
if (value == null) {
|
||||
value = getValue(tag.name, selectedFilterBuilderProp);
|
||||
}
|
||||
|
||||
onChange({
|
||||
name: NAME,
|
||||
value: [...filterValue, id]
|
||||
value: [...filterValue, value]
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -52,6 +103,7 @@ class FilterBuilderRowValue extends Component {
|
|||
render() {
|
||||
const {
|
||||
filterValue,
|
||||
selectedFilterBuilderProp,
|
||||
tagList
|
||||
} = this.props;
|
||||
|
||||
|
@ -68,7 +120,7 @@ class FilterBuilderRowValue extends Component {
|
|||
}
|
||||
return {
|
||||
id,
|
||||
name: id
|
||||
name: getTagDisplayValue(id, selectedFilterBuilderProp)
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -2,29 +2,70 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import styles from './CustomFilter.css';
|
||||
|
||||
class CustomFilter extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isDeleting: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
isDeleting,
|
||||
deleteError
|
||||
} = this.props;
|
||||
|
||||
if (prevProps.isDeleting && !isDeleting && this.state.isDeleting && deleteError) {
|
||||
this.setState({ isDeleting: false });
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const {
|
||||
id,
|
||||
selectedFilterKey,
|
||||
dispatchSetFilter
|
||||
} = this.props;
|
||||
|
||||
// Assume that delete and then unmounting means the delete was successful.
|
||||
// Moving this check to a ancestor would be more accurate, but would have
|
||||
// more boilerplate.
|
||||
if (this.state.isDeleting && id === selectedFilterKey) {
|
||||
dispatchSetFilter({ selectedFilterKey: 'all' });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditPress = () => {
|
||||
const {
|
||||
customFilterKey,
|
||||
id,
|
||||
onEditPress
|
||||
} = this.props;
|
||||
|
||||
onEditPress(customFilterKey);
|
||||
onEditPress(id);
|
||||
}
|
||||
|
||||
onRemovePress = () => {
|
||||
const {
|
||||
customFilterKey,
|
||||
onRemovePress
|
||||
id,
|
||||
dispatchDeleteCustomFilter
|
||||
} = this.props;
|
||||
|
||||
onRemovePress({ key: customFilterKey });
|
||||
this.setState({ isDeleting: true }, () => {
|
||||
dispatchDeleteCustomFilter({ id });
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -47,8 +88,9 @@ class CustomFilter extends Component {
|
|||
onPress={this.onEditPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
<SpinnerIconButton
|
||||
name={icons.REMOVE}
|
||||
isSpinning={this.state.isDeleting}
|
||||
onPress={this.onRemovePress}
|
||||
/>
|
||||
</div>
|
||||
|
@ -58,10 +100,14 @@ class CustomFilter extends Component {
|
|||
}
|
||||
|
||||
CustomFilter.propTypes = {
|
||||
customFilterKey: PropTypes.string.isRequired,
|
||||
id: PropTypes.number.isRequired,
|
||||
label: PropTypes.string.isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
dispatchSetFilter: PropTypes.func.isRequired,
|
||||
onEditPress: PropTypes.func.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired
|
||||
dispatchDeleteCustomFilter: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default CustomFilter;
|
||||
|
|
|
@ -10,9 +10,13 @@ import styles from './CustomFiltersModalContent.css';
|
|||
|
||||
function CustomFiltersModalContent(props) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
customFilters,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
dispatchDeleteCustomFilter,
|
||||
dispatchSetFilter,
|
||||
onAddCustomFilter,
|
||||
onRemoveCustomFilterPress,
|
||||
onEditCustomFilter,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
@ -29,10 +33,14 @@ function CustomFiltersModalContent(props) {
|
|||
return (
|
||||
<CustomFilter
|
||||
key={index}
|
||||
customFilterKey={customFilter.key}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
onRemovePress={onRemoveCustomFilterPress}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
|
@ -58,9 +66,13 @@ function CustomFiltersModalContent(props) {
|
|||
}
|
||||
|
||||
CustomFiltersModalContent.propTypes = {
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
dispatchDeleteCustomFilter: PropTypes.func.isRequired,
|
||||
dispatchSetFilter: PropTypes.func.isRequired,
|
||||
onAddCustomFilter: PropTypes.func.isRequired,
|
||||
onRemoveCustomFilterPress: PropTypes.func.isRequired,
|
||||
onEditCustomFilter: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteCustomFilter } from 'Store/Actions/customFilterActions';
|
||||
import CustomFiltersModalContent from './CustomFiltersModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.customFilters.isDeleting,
|
||||
(state) => state.customFilters.deleteError,
|
||||
(isDeleting, deleteError) => {
|
||||
return {
|
||||
isDeleting,
|
||||
deleteError
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchDeleteCustomFilter: deleteCustomFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(CustomFiltersModalContent);
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import FilterBuilderModalContentConnector from './Builder/FilterBuilderModalContentConnector';
|
||||
import CustomFiltersModalContent from './CustomFilters/CustomFiltersModalContent';
|
||||
import CustomFiltersModalContentConnector from './CustomFilters/CustomFiltersModalContentConnector';
|
||||
|
||||
class FilterModal extends Component {
|
||||
|
||||
|
@ -14,7 +14,7 @@ class FilterModal extends Component {
|
|||
|
||||
this.state = {
|
||||
filterBuilder: !props.customFilters.length,
|
||||
customFilterKey: null
|
||||
id: null
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -27,17 +27,17 @@ class FilterModal extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
onEditCustomFilter = (customFilterKey) => {
|
||||
onEditCustomFilter = (id) => {
|
||||
this.setState({
|
||||
filterBuilder: true,
|
||||
customFilterKey
|
||||
id
|
||||
});
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({
|
||||
filterBuilder: false,
|
||||
customFilterKey: null
|
||||
id: null
|
||||
}, () => {
|
||||
this.props.onModalClose();
|
||||
});
|
||||
|
@ -54,7 +54,7 @@ class FilterModal extends Component {
|
|||
|
||||
const {
|
||||
filterBuilder,
|
||||
customFilterKey
|
||||
id
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
|
@ -66,10 +66,10 @@ class FilterModal extends Component {
|
|||
filterBuilder ?
|
||||
<FilterBuilderModalContentConnector
|
||||
{...otherProps}
|
||||
customFilterKey={customFilterKey}
|
||||
id={id}
|
||||
onModalClose={this.onModalClose}
|
||||
/> :
|
||||
<CustomFiltersModalContent
|
||||
<CustomFiltersModalContentConnector
|
||||
{...otherProps}
|
||||
onAddCustomFilter={this.onAddCustomFilter}
|
||||
onEditCustomFilter={this.onEditCustomFilter}
|
||||
|
|
|
@ -2,7 +2,6 @@ import _ from 'lodash';
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Measure from 'react-measure';
|
||||
import TetherComponent from 'react-tether';
|
||||
import classNames from 'classnames';
|
||||
import isMobileUtil from 'Utilities/isMobile';
|
||||
|
@ -10,6 +9,7 @@ import * as keyCodes from 'Utilities/Constants/keyCodes';
|
|||
import { icons, scrollDirections } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Measure from 'Components/Measure';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
|
|
|
@ -62,6 +62,7 @@ class PathInput extends Component {
|
|||
event.preventDefault();
|
||||
const path = this.props.paths[0];
|
||||
|
||||
if (path) {
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value: path.path
|
||||
|
@ -72,6 +73,7 @@ class PathInput extends Component {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onInputBlur = () => {
|
||||
this.props.onClearPaths();
|
||||
|
|
|
@ -39,6 +39,10 @@ class TagInput extends Component {
|
|||
this._autosuggestRef = null;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.addTag.cancel();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactMeasure from 'react-measure';
|
||||
|
||||
class Measure extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentWillUnmount() {
|
||||
this.onMeasure.cancel();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMeasure = _.debounce((payload) => {
|
||||
this.props.onMeasure(payload);
|
||||
}, 250, { leading: true, trailing: false })
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ReactMeasure
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Measure.propTypes = {
|
||||
onMeasure: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Measure;
|
|
@ -42,6 +42,7 @@ class FilterMenu extends Component {
|
|||
customFilters,
|
||||
buttonComponent: ButtonComponent,
|
||||
filterModalConnectorComponent: FilterModalConnectorComponent,
|
||||
filterModalConnectorComponentProps,
|
||||
onFilterSelect,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
@ -74,6 +75,7 @@ class FilterMenu extends Component {
|
|||
{
|
||||
showCustomFilters &&
|
||||
<FilterModalConnectorComponent
|
||||
{...filterModalConnectorComponentProps}
|
||||
isOpen={this.state.isFilterModalOpen}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
|
@ -90,11 +92,12 @@ class FilterMenu extends Component {
|
|||
FilterMenu.propTypes = {
|
||||
className: PropTypes.string,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
buttonComponent: PropTypes.func.isRequired,
|
||||
filterModalConnectorComponent: PropTypes.func,
|
||||
filterModalConnectorComponentProps: PropTypes.object,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -42,8 +42,8 @@ class FilterMenuContent extends Component {
|
|||
customFilters.map((filter) => {
|
||||
return (
|
||||
<FilterMenuItem
|
||||
key={filter.key}
|
||||
filterKey={filter.key}
|
||||
key={filter.id}
|
||||
filterKey={filter.id}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
|
@ -70,7 +70,7 @@ class FilterMenuContent extends Component {
|
|||
}
|
||||
|
||||
FilterMenuContent.propTypes = {
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
showCustomFilters: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -37,8 +37,8 @@ class FilterMenuItem extends Component {
|
|||
}
|
||||
|
||||
FilterMenuItem.propTypes = {
|
||||
filterKey: PropTypes.string.isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
filterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
onPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ import elementClass from 'element-class';
|
|||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import ErrorBoundary from 'Components/Error/ErrorBoundary';
|
||||
import ModalError from './ModalError';
|
||||
import styles from './Modal.css';
|
||||
|
||||
const openModals = [];
|
||||
|
@ -153,7 +155,8 @@ class Modal extends Component {
|
|||
backdropClassName,
|
||||
size,
|
||||
children,
|
||||
isOpen
|
||||
isOpen,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
if (!isOpen) {
|
||||
|
@ -176,8 +179,13 @@ class Modal extends Component {
|
|||
styles[size]
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<ErrorBoundary
|
||||
errorComponent={ModalError}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
.message {
|
||||
composes: message from 'Components/Error/ErrorBoundaryError.css';
|
||||
|
||||
margin: 0;
|
||||
margin-bottom: 30px;
|
||||
font-weight: normal;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.details {
|
||||
composes: details from 'Components/Error/ErrorBoundaryError.css';
|
||||
|
||||
margin: 0;
|
||||
margin-top: 20px;
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './ModalError.css';
|
||||
|
||||
function ModalError(props) {
|
||||
const {
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Error
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<ErrorBoundaryError
|
||||
messageClassName={styles.message}
|
||||
detailsClassName={styles.details}
|
||||
{...otherProps}
|
||||
message='There was an error loading this item'
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>);
|
||||
}
|
||||
|
||||
ModalError.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ModalError;
|
|
@ -8,8 +8,11 @@ function ErrorPage(props) {
|
|||
version,
|
||||
isLocalStorageSupported,
|
||||
artistError,
|
||||
customFiltersError,
|
||||
tagsError,
|
||||
qualityProfilesError,
|
||||
languageProfilesError,
|
||||
metadataProfilesError,
|
||||
uiSettingsError
|
||||
} = props;
|
||||
|
||||
|
@ -19,10 +22,16 @@ function ErrorPage(props) {
|
|||
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
|
||||
} else if (artistError) {
|
||||
errorMessage = getErrorMessage(artistError, 'Failed to load artist from API');
|
||||
} else if (customFiltersError) {
|
||||
errorMessage = getErrorMessage(customFiltersError, 'Failed to load custom filters from API');
|
||||
} else if (tagsError) {
|
||||
errorMessage = getErrorMessage(artistError, 'Failed to load artist from API');
|
||||
errorMessage = getErrorMessage(tagsError, 'Failed to load tags from API');
|
||||
} else if (qualityProfilesError) {
|
||||
errorMessage = getErrorMessage(qualityProfilesError, 'Failed to load quality profiles from API');
|
||||
} else if (languageProfilesError) {
|
||||
errorMessage = getErrorMessage(languageProfilesError, 'Failed to load language profiles from API');
|
||||
} else if (metadataProfilesError) {
|
||||
errorMessage = getErrorMessage(metadataProfilesError, 'Failed to load metadata profiles from API');
|
||||
} else if (uiSettingsError) {
|
||||
errorMessage = getErrorMessage(uiSettingsError, 'Failed to load UI settings from API');
|
||||
}
|
||||
|
@ -44,8 +53,11 @@ ErrorPage.propTypes = {
|
|||
version: PropTypes.string.isRequired,
|
||||
isLocalStorageSupported: PropTypes.bool.isRequired,
|
||||
artistError: PropTypes.object,
|
||||
customFiltersError: PropTypes.object,
|
||||
tagsError: PropTypes.object,
|
||||
qualityProfilesError: PropTypes.object,
|
||||
languageProfilesError: PropTypes.object,
|
||||
metadataProfilesError: PropTypes.object,
|
||||
uiSettingsError: PropTypes.object
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import { withRouter } from 'react-router-dom';
|
|||
import { createSelector } from 'reselect';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||
import { fetchArtist } from 'Store/Actions/artistActions';
|
||||
import { fetchTags } from 'Store/Actions/tagActions';
|
||||
import { fetchQualityProfiles, fetchLanguageProfiles, fetchMetadataProfiles, fetchUISettings, fetchImportLists } from 'Store/Actions/settingsActions';
|
||||
|
@ -30,13 +31,15 @@ function testLocalStorage() {
|
|||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.artist,
|
||||
(state) => state.customFilters,
|
||||
(state) => state.tags,
|
||||
(state) => state.settings,
|
||||
(state) => state.app,
|
||||
createDimensionsSelector(),
|
||||
(artist, tags, settings, app, dimensions) => {
|
||||
(artist, customFilters, tags, settings, app, dimensions) => {
|
||||
const isPopulated = (
|
||||
artist.isPopulated &&
|
||||
customFilters.isPopulated &&
|
||||
tags.isPopulated &&
|
||||
settings.qualityProfiles.isPopulated &&
|
||||
settings.languageProfiles.isPopulated &&
|
||||
|
@ -47,6 +50,7 @@ function createMapStateToProps() {
|
|||
|
||||
const hasError = !!(
|
||||
artist.error ||
|
||||
customFilters.error ||
|
||||
tags.error ||
|
||||
settings.qualityProfiles.error ||
|
||||
settings.languageProfiles.error ||
|
||||
|
@ -59,6 +63,7 @@ function createMapStateToProps() {
|
|||
isPopulated,
|
||||
hasError,
|
||||
artistError: artist.error,
|
||||
customFiltersError: tags.error,
|
||||
tagsError: tags.error,
|
||||
qualityProfilesError: settings.qualityProfiles.error,
|
||||
languageProfilesError: settings.languageProfiles.error,
|
||||
|
@ -80,6 +85,9 @@ function createMapDispatchToProps(dispatch, props) {
|
|||
dispatchFetchArtist() {
|
||||
dispatch(fetchArtist());
|
||||
},
|
||||
dispatchFetchCustomFilters() {
|
||||
dispatch(fetchCustomFilters());
|
||||
},
|
||||
dispatchFetchTags() {
|
||||
dispatch(fetchTags());
|
||||
},
|
||||
|
@ -126,6 +134,7 @@ class PageConnector extends Component {
|
|||
componentDidMount() {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.dispatchFetchArtist();
|
||||
this.props.dispatchFetchCustomFilters();
|
||||
this.props.dispatchFetchTags();
|
||||
this.props.dispatchFetchQualityProfiles();
|
||||
this.props.dispatchFetchLanguageProfiles();
|
||||
|
@ -190,6 +199,7 @@ PageConnector.propTypes = {
|
|||
hasError: PropTypes.bool.isRequired,
|
||||
isSidebarVisible: PropTypes.bool.isRequired,
|
||||
dispatchFetchArtist: PropTypes.func.isRequired,
|
||||
dispatchFetchCustomFilters: PropTypes.func.isRequired,
|
||||
dispatchFetchTags: PropTypes.func.isRequired,
|
||||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchLanguageProfiles: PropTypes.func.isRequired,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import DocumentTitle from 'react-document-title';
|
||||
import ErrorBoundary from 'Components/Error/ErrorBoundary';
|
||||
import PageContentError from './PageContentError';
|
||||
import styles from './PageContent.css';
|
||||
|
||||
function PageContent(props) {
|
||||
|
@ -11,11 +13,13 @@ function PageContent(props) {
|
|||
} = props;
|
||||
|
||||
return (
|
||||
<ErrorBoundary errorComponent={PageContentError}>
|
||||
<DocumentTitle title={title ? `${title} - Lidarr` : 'Lidarr'}>
|
||||
<div className={className}>
|
||||
{children}
|
||||
</div>
|
||||
</DocumentTitle>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
.content {
|
||||
composes: content from './PageContent.css';
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import React from 'react';
|
||||
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
|
||||
import PageContentBodyConnector from './PageContentBodyConnector';
|
||||
import styles from './PageContentError.css';
|
||||
|
||||
function PageContentError(props) {
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<PageContentBodyConnector>
|
||||
<ErrorBoundaryError
|
||||
{...props}
|
||||
message='There was an error loading this page'
|
||||
/>
|
||||
</PageContentBodyConnector>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageContentError;
|
|
@ -1,8 +1,8 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Measure from 'react-measure';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import Measure from 'Components/Measure';
|
||||
import PageJumpBarItem from './PageJumpBarItem';
|
||||
import styles from './PageJumpBar.css';
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Measure from 'react-measure';
|
||||
import classNames from 'classnames';
|
||||
import { forEach } from 'Helpers/elementChildren';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import SpinnerIcon from 'Components/SpinnerIcon';
|
||||
import Measure from 'Components/Measure';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import MenuItem from 'Components/Menu/MenuItem';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Measure from 'react-measure';
|
||||
import { WindowScroller } from 'react-virtualized';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import Measure from 'Components/Measure';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import VirtualTableBody from './VirtualTableBody';
|
||||
import styles from './VirtualTable.css';
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 160 KiB |
|
@ -1,4 +1,5 @@
|
|||
export const BOOL = 'bool';
|
||||
export const BYTES = 'bytes';
|
||||
export const DATE = 'date';
|
||||
export const DEFAULT = 'default';
|
||||
export const INDEXER = 'indexer';
|
||||
|
|
|
@ -9,7 +9,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|||
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
||||
import TrackQuality from 'Album/TrackQuality';
|
||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
||||
import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal';
|
||||
import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal';
|
||||
|
@ -248,7 +248,7 @@ class InteractiveImportRow extends Component {
|
|||
|
||||
{
|
||||
!showQualityPlaceholder && !!quality &&
|
||||
<EpisodeQuality
|
||||
<TrackQuality
|
||||
className={styles.label}
|
||||
quality={quality}
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as releaseActions from 'Store/Actions/releaseActions';
|
||||
import { setReleasesFilter } from 'Store/Actions/releaseActions';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
@ -10,7 +10,8 @@ function createMapStateToProps() {
|
|||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps
|
||||
filterBuilderProps,
|
||||
customFilterType: 'releases'
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -18,12 +19,10 @@ function createMapStateToProps() {
|
|||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRemoveCustomFilterPress(payload) {
|
||||
dispatch(releaseActions.removeReleasesCustomFilter(payload));
|
||||
},
|
||||
dispatchSetFilter(payload) {
|
||||
const action = setReleasesFilter;
|
||||
|
||||
onSaveCustomFilterPress(payload) {
|
||||
dispatch(releaseActions.saveReleasesCustomFilter(payload));
|
||||
dispatch(action(payload));
|
||||
}
|
||||
};
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import InteractiveAlbumSearchModalContentConnector from './InteractiveAlbumSearchModalContentConnector';
|
||||
import InteractiveSearchModalContentConnector from './InteractiveSearchModalContentConnector';
|
||||
|
||||
function InteractiveAlbumSearchModal(props) {
|
||||
function InteractiveSearchModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
|
@ -15,7 +15,7 @@ function InteractiveAlbumSearchModal(props) {
|
|||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<InteractiveAlbumSearchModalContentConnector
|
||||
<InteractiveSearchModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
|
@ -23,9 +23,9 @@ function InteractiveAlbumSearchModal(props) {
|
|||
);
|
||||
}
|
||||
|
||||
InteractiveAlbumSearchModal.propTypes = {
|
||||
InteractiveSearchModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default InteractiveAlbumSearchModal;
|
||||
export default InteractiveSearchModal;
|
|
@ -13,8 +13,8 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
|
||||
import InteractiveAlbumSearchRow from './InteractiveAlbumSearchRow';
|
||||
import styles from './InteractiveAlbumSearchModalContent.css';
|
||||
import InteractiveSearchRow from './InteractiveSearchRow';
|
||||
import styles from './InteractiveSearchModalContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
|
@ -75,7 +75,7 @@ const columns = [
|
|||
}
|
||||
];
|
||||
|
||||
class InteractiveAlbumSearchModalContent extends Component {
|
||||
class InteractiveSearchModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
@ -161,7 +161,7 @@ class InteractiveAlbumSearchModalContent extends Component {
|
|||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<InteractiveAlbumSearchRow
|
||||
<InteractiveSearchRow
|
||||
key={item.guid}
|
||||
{...item}
|
||||
longDateFormat={longDateFormat}
|
||||
|
@ -195,7 +195,7 @@ class InteractiveAlbumSearchModalContent extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
InteractiveAlbumSearchModalContent.propTypes = {
|
||||
InteractiveSearchModalContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
|
@ -203,7 +203,7 @@ InteractiveAlbumSearchModalContent.propTypes = {
|
|||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
|
@ -214,4 +214,4 @@ InteractiveAlbumSearchModalContent.propTypes = {
|
|||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default InteractiveAlbumSearchModalContent;
|
||||
export default InteractiveSearchModalContent;
|
|
@ -5,7 +5,7 @@ import { createSelector } from 'reselect';
|
|||
import * as releaseActions from 'Store/Actions/releaseActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import InteractiveAlbumSearchModalContent from './InteractiveAlbumSearchModalContent';
|
||||
import InteractiveSearchModalContent from './InteractiveSearchModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
|
@ -51,7 +51,7 @@ function createMapDispatchToProps(dispatch, props) {
|
|||
};
|
||||
}
|
||||
|
||||
class InteractiveAlbumSearchModalContentConnector extends Component {
|
||||
class InteractiveSearchModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
@ -81,18 +81,18 @@ class InteractiveAlbumSearchModalContentConnector extends Component {
|
|||
} = this.props;
|
||||
|
||||
return (
|
||||
<InteractiveAlbumSearchModalContent
|
||||
<InteractiveSearchModalContent
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveAlbumSearchModalContentConnector.propTypes = {
|
||||
InteractiveSearchModalContentConnector.propTypes = {
|
||||
albumId: PropTypes.number,
|
||||
dispatchFetchReleases: PropTypes.func.isRequired,
|
||||
dispatchClearReleases: PropTypes.func.isRequired,
|
||||
dispatchCancelFetchReleases: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveAlbumSearchModalContentConnector);
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(InteractiveSearchModalContentConnector);
|
|
@ -10,10 +10,10 @@ import Link from 'Components/Link/Link';
|
|||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
||||
import TrackQuality from 'Album/TrackQuality';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import Peers from './Peers';
|
||||
import styles from './InteractiveAlbumSearchRow.css';
|
||||
import styles from './InteractiveSearchRow.css';
|
||||
|
||||
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
||||
if (isGrabbing) {
|
||||
|
@ -39,7 +39,7 @@ function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
|||
return 'Add to downloaded queue';
|
||||
}
|
||||
|
||||
class InteractiveAlbumSearchRow extends Component {
|
||||
class InteractiveSearchRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
@ -120,7 +120,7 @@ class InteractiveAlbumSearchRow extends Component {
|
|||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.quality}>
|
||||
<EpisodeQuality
|
||||
<TrackQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
@ -171,7 +171,7 @@ class InteractiveAlbumSearchRow extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
InteractiveAlbumSearchRow.propTypes = {
|
||||
InteractiveSearchRow.propTypes = {
|
||||
guid: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
age: PropTypes.number.isRequired,
|
||||
|
@ -196,9 +196,9 @@ InteractiveAlbumSearchRow.propTypes = {
|
|||
onGrabPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
InteractiveAlbumSearchRow.defaultProps = {
|
||||
InteractiveSearchRow.defaultProps = {
|
||||
isGrabbing: false,
|
||||
isGrabbed: false
|
||||
};
|
||||
|
||||
export default InteractiveAlbumSearchRow;
|
||||
export default InteractiveSearchRow;
|
|
@ -1,10 +1,10 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Measure from 'react-measure';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Measure from 'Components/Measure';
|
||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||
import DelayProfileDragSource from './DelayProfileDragSource';
|
||||
import DelayProfileDragPreview from './DelayProfileDragPreview';
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Measure from 'react-measure';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Measure from 'Components/Measure';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Measure from 'react-measure';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
||||
import Measure from 'Components/Measure';
|
||||
import QualityProfileItemDragSource from './QualityProfileItemDragSource';
|
||||
import QualityProfileItemDragPreview from './QualityProfileItemDragPreview';
|
||||
import styles from './QualityProfileItems.css';
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
import customFilterHandlers from 'Utilities/customFilterHandlers';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import generateUUIDv4 from 'Utilities/String/generateUUIDv4';
|
||||
|
||||
function createRemoveCustomFilterReducer(section) {
|
||||
return (state, { payload }) => {
|
||||
const newState = getSectionState(state, section);
|
||||
const index = newState.customFilters.findIndex((c) => c.key === payload.key);
|
||||
|
||||
newState.customFilters = [...newState.customFilters];
|
||||
newState.customFilters.splice(index, 1);
|
||||
|
||||
// Reset the selected filter to the first filter if the selected filter
|
||||
// is being deleted.
|
||||
// TODO: Server side collections need to have their collections refetched
|
||||
|
||||
if (newState.selectedFilterKey === payload.key) {
|
||||
newState.selectedFilterKey = newState.filters[0].key;
|
||||
}
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
};
|
||||
}
|
||||
|
||||
function createSaveCustomFilterReducer(section) {
|
||||
return (state, { payload }) => {
|
||||
const newState = getSectionState(state, section);
|
||||
|
||||
const {
|
||||
label,
|
||||
filters
|
||||
} = payload;
|
||||
|
||||
let key = payload.key;
|
||||
|
||||
newState.customFilters = [...newState.customFilters];
|
||||
|
||||
if (key) {
|
||||
const index = newState.customFilters.findIndex((c) => c.key === key);
|
||||
|
||||
newState.customFilters.splice(index, 1, { key, label, filters });
|
||||
} else {
|
||||
key = generateUUIDv4();
|
||||
|
||||
newState.customFilters.push({
|
||||
key,
|
||||
label,
|
||||
filters
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Server side collections need to have their collections refetched
|
||||
newState.selectedFilterKey = key;
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
};
|
||||
}
|
||||
|
||||
export default function createCustomFilterReducers(section, handlers) {
|
||||
return {
|
||||
[handlers[customFilterHandlers.REMOVE]]: createRemoveCustomFilterReducer(section),
|
||||
[handlers[customFilterHandlers.SAVE]]: createSaveCustomFilterReducer(section)
|
||||
};
|
||||
}
|
|
@ -20,13 +20,13 @@ function createRemoveItemHandler(section, url) {
|
|||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
removeItem({ section, id }),
|
||||
|
||||
set({
|
||||
section,
|
||||
isDeleting: false,
|
||||
deleteError: null
|
||||
})
|
||||
}),
|
||||
|
||||
removeItem({ section, id })
|
||||
]));
|
||||
});
|
||||
|
||||
|
|
|
@ -21,10 +21,11 @@ function createSaveProviderHandler(section, url, options = {}) {
|
|||
|
||||
const {
|
||||
id,
|
||||
queryParams = {}
|
||||
queryParams = {},
|
||||
...otherPayload
|
||||
} = payload;
|
||||
|
||||
const saveData = getProviderState(payload, getState, section);
|
||||
const saveData = getProviderState({ id, ...otherPayload }, getState, section);
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${url}?${$.param(queryParams, true)}`,
|
||||
|
|
|
@ -2,7 +2,7 @@ import _ from 'lodash';
|
|||
import $ from 'jquery';
|
||||
import { createAction } from 'redux-actions';
|
||||
import getMonitoringOptions from 'Utilities/Artist/getMonitoringOptions';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
||||
|
@ -29,7 +29,55 @@ export const defaultState = {
|
|||
selectedFilterKey: 'all',
|
||||
filters,
|
||||
filterPredicates,
|
||||
customFilters: []
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'monitored',
|
||||
label: 'Monitored',
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.BOOL
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: 'Status',
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.ARTIST_STATUS
|
||||
},
|
||||
{
|
||||
name: 'artistType',
|
||||
label: 'Artist Type',
|
||||
type: filterBuilderTypes.EXACT
|
||||
},
|
||||
{
|
||||
name: 'qualityProfileId',
|
||||
label: 'Quality Profile',
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.QUALITY_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'languageProfileId',
|
||||
label: 'Language Profile',
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.LANGUAGE_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'metadataProfileId',
|
||||
label: 'Metadata Profile',
|
||||
type: filterBuilderTypes.EXACT,
|
||||
valueType: filterBuilderValueTypes.METADATA_PROFILE
|
||||
},
|
||||
{
|
||||
name: 'rootFolderPath',
|
||||
label: 'Root Folder Path',
|
||||
type: filterBuilderTypes.EXACT
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
type: filterBuilderTypes.ARRAY,
|
||||
valueType: filterBuilderValueTypes.TAG
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
|
|
|
@ -103,6 +103,20 @@ export const filterPredicates = {
|
|||
const predicate = filterTypePredicates[type];
|
||||
|
||||
return predicate(item.ratings.value * 10, filterValue);
|
||||
},
|
||||
|
||||
albumCount: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
const albumCount = item.statistics ? item.statistics.albumCount : 0;
|
||||
|
||||
return predicate(albumCount, filterValue);
|
||||
},
|
||||
|
||||
sizeOnDisk: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
const sizeOnDisk = item.statistics ? item.statistics.sizeOnDisk : 0;
|
||||
|
||||
return predicate(sizeOnDisk, filterValue);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import $ from 'jquery';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import customFilterHandlers from 'Utilities/customFilterHandlers';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
||||
import createCustomFilterReducers from './Creators/Reducers/createCustomFilterReducers';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import { set, updateItem } from './baseActions';
|
||||
import { filters, filterPredicates } from './artistActions';
|
||||
|
@ -79,8 +77,7 @@ export const defaultState = {
|
|||
type: filterBuilderTypes.ARRAY,
|
||||
valueType: filterBuilderValueTypes.TAG
|
||||
}
|
||||
],
|
||||
customFilters: []
|
||||
]
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
|
@ -97,8 +94,6 @@ export const SET_ARTIST_EDITOR_SORT = 'artistEditor/setArtistEditorSort';
|
|||
export const SET_ARTIST_EDITOR_FILTER = 'artistEditor/setArtistEditorFilter';
|
||||
export const SAVE_ARTIST_EDITOR = 'artistEditor/saveArtistEditor';
|
||||
export const BULK_DELETE_ARTIST = 'artistEditor/bulkDeleteArtist';
|
||||
export const REMOVE_ARTIST_EDITOR_CUSTOM_FILTER = 'artistEditor/removeArtistEditorCustomFilter';
|
||||
export const SAVE_ARTIST_EDITOR_CUSTOM_FILTER = 'artistEditor/saveArtistEditorCustomFilter';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
@ -107,8 +102,6 @@ export const setArtistEditorSort = createAction(SET_ARTIST_EDITOR_SORT);
|
|||
export const setArtistEditorFilter = createAction(SET_ARTIST_EDITOR_FILTER);
|
||||
export const saveArtistEditor = createThunk(SAVE_ARTIST_EDITOR);
|
||||
export const bulkDeleteArtist = createThunk(BULK_DELETE_ARTIST);
|
||||
export const removeArtistEditorCustomFilter = createAction(REMOVE_ARTIST_EDITOR_CUSTOM_FILTER);
|
||||
export const saveArtistEditorCustomFilter = createAction(SAVE_ARTIST_EDITOR_CUSTOM_FILTER);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
@ -193,11 +186,6 @@ export const actionHandlers = handleThunks({
|
|||
export const reducers = createHandleActions({
|
||||
|
||||
[SET_ARTIST_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section),
|
||||
[SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section),
|
||||
|
||||
...createCustomFilterReducers(section, {
|
||||
[customFilterHandlers.REMOVE]: REMOVE_ARTIST_EDITOR_CUSTOM_FILTER,
|
||||
[customFilterHandlers.SAVE]: SAVE_ARTIST_EDITOR_CUSTOM_FILTER
|
||||
})
|
||||
[SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section)
|
||||
|
||||
}, defaultState, section);
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import moment from 'moment';
|
||||
import { createAction } from 'redux-actions';
|
||||
import customFilterHandlers from 'Utilities/customFilterHandlers';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
||||
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
||||
import createCustomFilterReducers from './Creators/Reducers/createCustomFilterReducers';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import { filters, filterPredicates } from './artistActions';
|
||||
|
||||
|
@ -292,7 +290,8 @@ export const defaultState = {
|
|||
{
|
||||
name: 'sizeOnDisk',
|
||||
label: 'Size on Disk',
|
||||
type: filterBuilderTypes.NUMBER
|
||||
type: filterBuilderTypes.NUMBER,
|
||||
valueType: filterBuilderValueTypes.BYTES
|
||||
},
|
||||
{
|
||||
name: 'genres',
|
||||
|
@ -324,8 +323,7 @@ export const defaultState = {
|
|||
type: filterBuilderTypes.ARRAY,
|
||||
valueType: filterBuilderValueTypes.TAG
|
||||
}
|
||||
],
|
||||
customFilters: []
|
||||
]
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
|
@ -350,8 +348,6 @@ export const SET_ARTIST_TABLE_OPTION = 'artistIndex/setArtistTableOption';
|
|||
export const SET_ARTIST_POSTER_OPTION = 'artistIndex/setArtistPosterOption';
|
||||
export const SET_ARTIST_BANNER_OPTION = 'artistIndex/setArtistBannerOption';
|
||||
export const SET_ARTIST_OVERVIEW_OPTION = 'artistIndex/setArtistOverviewOption';
|
||||
export const REMOVE_ARTIST_CUSTOM_FILTER = 'artistIndex/removeArtistCustomFilter';
|
||||
export const SAVE_ARTIST_CUSTOM_FILTER = 'artistIndex/saveArtistCustomFilter';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
@ -363,8 +359,7 @@ export const setArtistTableOption = createAction(SET_ARTIST_TABLE_OPTION);
|
|||
export const setArtistPosterOption = createAction(SET_ARTIST_POSTER_OPTION);
|
||||
export const setArtistBannerOption = createAction(SET_ARTIST_BANNER_OPTION);
|
||||
export const setArtistOverviewOption = createAction(SET_ARTIST_OVERVIEW_OPTION);
|
||||
export const removeArtistCustomFilter = createAction(REMOVE_ARTIST_CUSTOM_FILTER);
|
||||
export const saveArtistCustomFilter = createAction(SAVE_ARTIST_CUSTOM_FILTER);
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
|
@ -413,11 +408,6 @@ export const reducers = createHandleActions({
|
|||
...payload
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
...createCustomFilterReducers(section, {
|
||||
[customFilterHandlers.REMOVE]: REMOVE_ARTIST_CUSTOM_FILTER,
|
||||
[customFilterHandlers.SAVE]: SAVE_ARTIST_CUSTOM_FILTER
|
||||
})
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'customFilters';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_CUSTOM_FILTERS = 'customFilters/fetchCustomFilters';
|
||||
export const SAVE_CUSTOM_FILTER = 'customFilters/saveCustomFilter';
|
||||
export const DELETE_CUSTOM_FILTER = 'customFilters/deleteCustomFilter';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchCustomFilters = createThunk(FETCH_CUSTOM_FILTERS);
|
||||
export const saveCustomFilter = createThunk(SAVE_CUSTOM_FILTER);
|
||||
export const deleteCustomFilter = createThunk(DELETE_CUSTOM_FILTER);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_CUSTOM_FILTERS]: createFetchHandler(section, '/customFilter'),
|
||||
|
||||
[SAVE_CUSTOM_FILTER]: createSaveProviderHandler(section, '/customFilter'),
|
||||
|
||||
[DELETE_CUSTOM_FILTER]: createRemoveItemHandler(section, '/customFilter')
|
||||
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
export const reducers = createHandleActions({}, defaultState, section);
|
|
@ -2,6 +2,7 @@ import * as addArtist from './addArtistActions';
|
|||
import * as app from './appActions';
|
||||
import * as blacklist from './blacklistActions';
|
||||
import * as captcha from './captchaActions';
|
||||
import * as customFilters from './customFilterActions';
|
||||
import * as devices from './deviceActions';
|
||||
import * as calendar from './calendarActions';
|
||||
import * as commands from './commandActions';
|
||||
|
@ -35,6 +36,7 @@ export default [
|
|||
captcha,
|
||||
calendar,
|
||||
commands,
|
||||
customFilters,
|
||||
devices,
|
||||
albums,
|
||||
trackFiles,
|
||||
|
|
|
@ -10,6 +10,7 @@ import createHandleActions from './Creators/createHandleActions';
|
|||
// Variables
|
||||
|
||||
export const section = 'oAuth';
|
||||
const callbackUrl = `${window.location.origin}${window.Lidarr.urlBase}/oauth.html`;
|
||||
|
||||
//
|
||||
// State
|
||||
|
@ -64,6 +65,19 @@ function showOAuthWindow(url) {
|
|||
return deferred.promise();
|
||||
}
|
||||
|
||||
function executeIntermediateRequest(payload, ajaxOptions) {
|
||||
return $.ajax(ajaxOptions).then((data) => {
|
||||
return requestAction({
|
||||
action: 'continueOAuth',
|
||||
queryParams: {
|
||||
...data,
|
||||
callbackUrl
|
||||
},
|
||||
...payload
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
|
@ -72,7 +86,7 @@ export const actionHandlers = handleThunks({
|
|||
[START_OAUTH]: function(getState, payload, dispatch) {
|
||||
const actionPayload = {
|
||||
action: 'startOAuth',
|
||||
queryParams: { callbackUrl: `${window.location.origin}${window.Lidarr.urlBase}/oauth.html` },
|
||||
queryParams: { callbackUrl },
|
||||
...payload
|
||||
};
|
||||
|
||||
|
@ -85,7 +99,16 @@ export const actionHandlers = handleThunks({
|
|||
const promise = requestAction(actionPayload)
|
||||
.then((response) => {
|
||||
startResponse = response;
|
||||
|
||||
if (response.oauthUrl) {
|
||||
return showOAuthWindow(response.oauthUrl);
|
||||
}
|
||||
|
||||
return executeIntermediateRequest(payload, response).then((intermediateResponse) => {
|
||||
startResponse = intermediateResponse;
|
||||
|
||||
return showOAuthWindow(intermediateResponse.oauthUrl);
|
||||
});
|
||||
})
|
||||
.then((queryParams) => {
|
||||
return requestAction({
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import $ from 'jquery';
|
||||
import { createAction } from 'redux-actions';
|
||||
import customFilterHandlers from 'Utilities/customFilterHandlers';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
||||
import createCustomFilterReducers from './Creators/Reducers/createCustomFilterReducers';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
|
||||
|
@ -45,8 +43,6 @@ export const defaultState = {
|
|||
}
|
||||
},
|
||||
|
||||
selectedFilterKey: 'all',
|
||||
|
||||
filters: [
|
||||
{
|
||||
key: 'all',
|
||||
|
@ -143,9 +139,7 @@ export const defaultState = {
|
|||
label: 'Rejections',
|
||||
type: filterBuilderTypes.NUMBER
|
||||
}
|
||||
],
|
||||
|
||||
customFilters: []
|
||||
]
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
|
@ -163,9 +157,6 @@ export const CLEAR_RELEASES = 'releases/clearReleases';
|
|||
export const GRAB_RELEASE = 'releases/grabRelease';
|
||||
export const UPDATE_RELEASE = 'releases/updateRelease';
|
||||
export const SET_RELEASES_FILTER = 'releases/setReleasesFilter';
|
||||
export const ADD_RELEASES_CUSTOM_FILTER = 'releases/addReleasesCustomFilter';
|
||||
export const REMOVE_RELEASES_CUSTOM_FILTER = 'releases/removeReleasesCustomFilter';
|
||||
export const SAVE_RELEASES_CUSTOM_FILTER = 'releases/saveReleasesCustomFilter';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
@ -177,9 +168,6 @@ export const clearReleases = createAction(CLEAR_RELEASES);
|
|||
export const grabRelease = createThunk(GRAB_RELEASE);
|
||||
export const updateRelease = createAction(UPDATE_RELEASE);
|
||||
export const setReleasesFilter = createAction(SET_RELEASES_FILTER);
|
||||
export const addReleasesCustomFilter = createAction(ADD_RELEASES_CUSTOM_FILTER);
|
||||
export const removeReleasesCustomFilter = createAction(REMOVE_RELEASES_CUSTOM_FILTER);
|
||||
export const saveReleasesCustomFilter = createAction(SAVE_RELEASES_CUSTOM_FILTER);
|
||||
|
||||
//
|
||||
// Helpers
|
||||
|
@ -266,11 +254,6 @@ export const reducers = createHandleActions({
|
|||
},
|
||||
|
||||
[SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(section),
|
||||
[SET_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(section),
|
||||
|
||||
...createCustomFilterReducers(section, {
|
||||
[customFilterHandlers.REMOVE]: REMOVE_RELEASES_CUSTOM_FILTER,
|
||||
[customFilterHandlers.SAVE]: SAVE_RELEASES_CUSTOM_FILTER
|
||||
})
|
||||
[SET_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(section)
|
||||
|
||||
}, defaultState, section);
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import _ from 'lodash';
|
||||
import * as sentry from '@sentry/browser';
|
||||
import parseUrl from 'Utilities/String/parseUrl';
|
||||
|
||||
function cleanseUrl(url) {
|
||||
const properties = parseUrl(url);
|
||||
|
||||
return `${properties.pathname}${properties.search}`;
|
||||
}
|
||||
|
||||
function cleanseData(data) {
|
||||
const result = _.cloneDeep(data);
|
||||
|
||||
result.transaction = cleanseUrl(result.transaction);
|
||||
|
||||
if (result.exception) {
|
||||
result.exception.values.forEach((exception) => {
|
||||
const stacktrace = exception.stacktrace;
|
||||
|
||||
if (stacktrace) {
|
||||
stacktrace.frames.forEach((frame) => {
|
||||
frame.filename = cleanseUrl(frame.filename);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
result.request.url = cleanseUrl(result.request.url);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function identity(stuff) {
|
||||
return stuff;
|
||||
}
|
||||
|
||||
function createMiddleware() {
|
||||
return (store) => (next) => (action) => {
|
||||
try {
|
||||
// Adds a breadcrumb for reporting later (if necessary).
|
||||
sentry.addBreadcrumb({
|
||||
category: 'redux',
|
||||
message: action.type
|
||||
});
|
||||
|
||||
return next(action);
|
||||
} catch (err) {
|
||||
console.error(`[sentry] Reporting error to Sentry: ${err}`);
|
||||
|
||||
// Send the report including breadcrumbs.
|
||||
sentry.captureException(err, {
|
||||
extra: {
|
||||
action: identity(action),
|
||||
state: identity(store.getState())
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default function createSentryMiddleware() {
|
||||
const {
|
||||
analytics,
|
||||
branch,
|
||||
version,
|
||||
release,
|
||||
isProduction
|
||||
} = window.Lidarr;
|
||||
|
||||
if (!analytics) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dsn = isProduction ? 'https://c3a5b33e08de4e18b7d0505e942dbc95@sentry.io/216290' :
|
||||
'https://baede6f14da54cf48ff431479e400adf@sentry.io/1249427';
|
||||
|
||||
sentry.init({
|
||||
dsn,
|
||||
environment: isProduction ? 'production' : 'development',
|
||||
release,
|
||||
sendDefaultPii: true,
|
||||
beforeSend: cleanseData
|
||||
});
|
||||
|
||||
sentry.configureScope((scope) => {
|
||||
scope.setTag('branch', branch);
|
||||
scope.setTag('version', version);
|
||||
});
|
||||
|
||||
return createMiddleware();
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
import { applyMiddleware, compose } from 'redux';
|
||||
import thunk from 'redux-thunk';
|
||||
import { routerMiddleware } from 'react-router-redux';
|
||||
import sentryMiddleware from './sentryMiddleware';
|
||||
import createSentryMiddleware from './createSentryMiddleware';
|
||||
import persistState from './persistState';
|
||||
|
||||
export default function(history) {
|
||||
const middlewares = [];
|
||||
const ravenMiddleware = sentryMiddleware();
|
||||
const sentryMiddleware = createSentryMiddleware();
|
||||
|
||||
if (ravenMiddleware) {
|
||||
middlewares.push(ravenMiddleware);
|
||||
if (sentryMiddleware) {
|
||||
middlewares.push(sentryMiddleware);
|
||||
}
|
||||
|
||||
middlewares.push(routerMiddleware(history));
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import Raven from 'raven-js';
|
||||
import createRavenMiddleware from 'raven-for-redux';
|
||||
import parseUrl from 'Utilities/String/parseUrl';
|
||||
|
||||
function cleanseUrl(url) {
|
||||
const properties = parseUrl(url);
|
||||
|
||||
return `${properties.pathname}${properties.search}`;
|
||||
}
|
||||
|
||||
function cleanseData(data) {
|
||||
const result = _.cloneDeep(data);
|
||||
|
||||
result.culprit = cleanseUrl(result.culprit);
|
||||
result.request.url = cleanseUrl(result.request.url);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default function sentryMiddleware() {
|
||||
const {
|
||||
analytics,
|
||||
branch,
|
||||
version,
|
||||
release,
|
||||
isProduction
|
||||
} = window.Lidarr;
|
||||
|
||||
if (!analytics) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dsn = isProduction ? 'https://c3a5b33e08de4e18b7d0505e942dbc95@sentry.io/216290' :
|
||||
'https://baede6f14da54cf48ff431479e400adf@sentry.io/1249427';
|
||||
|
||||
Raven.config(
|
||||
dsn,
|
||||
{
|
||||
environment: isProduction ? 'production' : 'development',
|
||||
release,
|
||||
tags: {
|
||||
branch,
|
||||
version
|
||||
},
|
||||
dataCallback: cleanseData
|
||||
}
|
||||
).install();
|
||||
|
||||
return createRavenMiddleware(Raven);
|
||||
}
|
|
@ -94,12 +94,24 @@ function sort(items, state) {
|
|||
return _.orderBy(items, clauses, orders);
|
||||
}
|
||||
|
||||
function createCustomFiltersSelector(type, alternateType) {
|
||||
return createSelector(
|
||||
(state) => state.customFilters.items,
|
||||
(customFilters) => {
|
||||
return customFilters.filter((customFilter) => {
|
||||
return customFilter.type === type || customFilter.type === alternateType;
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createClientSideCollectionSelector(section, uiSection) {
|
||||
return createSelector(
|
||||
(state) => _.get(state, section),
|
||||
(state) => _.get(state, uiSection),
|
||||
(sectionState, uiSectionState = {}) => {
|
||||
const state = Object.assign({}, sectionState, uiSectionState);
|
||||
createCustomFiltersSelector(section, uiSection),
|
||||
(sectionState, uiSectionState = {}, customFilters) => {
|
||||
const state = Object.assign({}, sectionState, uiSectionState, { customFilters });
|
||||
|
||||
const filtered = filter(state.items, state);
|
||||
const sorted = sort(filtered, state);
|
||||
|
@ -107,6 +119,7 @@ function createClientSideCollectionSelector(section, uiSection) {
|
|||
return {
|
||||
...sectionState,
|
||||
...uiSectionState,
|
||||
customFilters,
|
||||
items: sorted,
|
||||
totalItems: state.items.length
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import titleCase from 'Utilities/String/titleCase';
|
|||
import FieldSet from 'Components/FieldSet';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import StartTime from './StartTime';
|
||||
import styles from './About.css';
|
||||
|
||||
class About extends Component {
|
||||
|
@ -19,7 +20,10 @@ class About extends Component {
|
|||
migrationVersion,
|
||||
appData,
|
||||
startupPath,
|
||||
mode
|
||||
mode,
|
||||
startTime,
|
||||
timeFormat,
|
||||
longDateFormat
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -57,6 +61,17 @@ class About extends Component {
|
|||
title="Mode"
|
||||
data={titleCase(mode)}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Uptime"
|
||||
data={
|
||||
<StartTime
|
||||
startTime={startTime}
|
||||
timeFormat={timeFormat}
|
||||
longDateFormat={longDateFormat}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</DescriptionList>
|
||||
</FieldSet>
|
||||
);
|
||||
|
@ -65,13 +80,16 @@ class About extends Component {
|
|||
}
|
||||
|
||||
About.propTypes = {
|
||||
version: PropTypes.string,
|
||||
isMonoRuntime: PropTypes.bool,
|
||||
runtimeVersion: PropTypes.string,
|
||||
migrationVersion: PropTypes.number,
|
||||
appData: PropTypes.string,
|
||||
startupPath: PropTypes.string,
|
||||
mode: PropTypes.string
|
||||
version: PropTypes.string.isRequired,
|
||||
isMonoRuntime: PropTypes.bool.isRequired,
|
||||
runtimeVersion: PropTypes.string.isRequired,
|
||||
migrationVersion: PropTypes.number.isRequired,
|
||||
appData: PropTypes.string.isRequired,
|
||||
startupPath: PropTypes.string.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
startTime: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default About;
|
||||
|
|
|
@ -3,14 +3,18 @@ import React, { Component } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import About from './About';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.system.status,
|
||||
(status) => {
|
||||
createUISettingsSelector(),
|
||||
(status, uiSettings) => {
|
||||
return {
|
||||
...status.item
|
||||
...status.item,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
longDateFormat: uiSettings.longDateFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
|
||||
function getUptime(startTime) {
|
||||
return formatTimeSpan(moment().diff(startTime));
|
||||
}
|
||||
|
||||
class StartTime extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const {
|
||||
startTime,
|
||||
timeFormat,
|
||||
longDateFormat
|
||||
} = props;
|
||||
|
||||
this._timeoutId = null;
|
||||
|
||||
this.state = {
|
||||
uptime: getUptime(startTime),
|
||||
startTime: formatDateTime(startTime, longDateFormat, timeFormat, { includeSeconds: true })
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._timeoutId = setTimeout(this.onTimeout, 1000);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
startTime,
|
||||
timeFormat,
|
||||
longDateFormat
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
startTime !== prevProps.startTime ||
|
||||
timeFormat !== prevProps.timeFormat ||
|
||||
longDateFormat !== prevProps.longDateFormat
|
||||
) {
|
||||
this.setState({
|
||||
uptime: getUptime(startTime),
|
||||
startTime: formatDateTime(startTime, longDateFormat, timeFormat, { includeSeconds: true })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._timeoutId) {
|
||||
this._timeoutId = clearTimeout(this._timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTimeout = () => {
|
||||
this.setState({ uptime: getUptime(this.props.startTime) });
|
||||
this._timeoutId = setTimeout(this.onTimeout, 1000);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
uptime,
|
||||
startTime
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<span title={startTime}>
|
||||
{uptime}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StartTime.propTypes = {
|
||||
startTime: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default StartTime;
|
|
@ -70,9 +70,15 @@ class TrackFileEditorModalContent extends Component {
|
|||
getSelectedIds = () => {
|
||||
const selectedIds = getSelectedIds(this.state.selectedState);
|
||||
|
||||
return _.uniq(_.map(selectedIds, (id) => {
|
||||
return _.find(this.props.items, { id }).trackFileId;
|
||||
}));
|
||||
return selectedIds.reduce((acc, id) => {
|
||||
const matchingItem = this.props.items.find((item) => item.id === id);
|
||||
|
||||
if (matchingItem && !acc.includes(matchingItem.trackFileID)) {
|
||||
acc.push(matchingItem.trackFileID);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
@ -5,7 +5,7 @@ import Label from 'Components/Label';
|
|||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
||||
import TrackQuality from 'Album/TrackQuality';
|
||||
import styles from './TrackFileEditorRow';
|
||||
|
||||
function TrackFileEditorRow(props) {
|
||||
|
@ -42,7 +42,7 @@ function TrackFileEditorRow(props) {
|
|||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeQuality
|
||||
<TrackQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
|
|
@ -3,7 +3,11 @@ export default function findSelectedFilters(selectedFilterKey, filters = [], cus
|
|||
return [];
|
||||
}
|
||||
|
||||
const selectedFilter = [...filters, ...customFilters].find((f) => f.key === selectedFilterKey);
|
||||
let selectedFilter = filters.find((f) => f.key === selectedFilterKey);
|
||||
|
||||
if (!selectedFilter) {
|
||||
selectedFilter = customFilters.find((f) => f.id === selectedFilterKey);
|
||||
}
|
||||
|
||||
if (!selectedFilter) {
|
||||
// TODO: throw in dev
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
function convertToBytes(input, power, binaryPrefix) {
|
||||
const size = Number(input);
|
||||
|
||||
if (isNaN(size)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const prefix = binaryPrefix ? 1024 : 1000;
|
||||
const multiplier = Math.pow(prefix, power);
|
||||
const result = size * multiplier;
|
||||
|
||||
return Math.round(result);
|
||||
}
|
||||
|
||||
export default convertToBytes;
|
|
@ -2,9 +2,12 @@ import _ from 'lodash';
|
|||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
|
||||
function getProviderState(payload, getState, section) {
|
||||
const id = payload.id;
|
||||
const {
|
||||
id,
|
||||
...otherPayload
|
||||
} = payload;
|
||||
const state = getSectionState(getState(), section, true);
|
||||
const pendingChanges = Object.assign({}, state.pendingChanges);
|
||||
const pendingChanges = Object.assign({}, state.pendingChanges, otherPayload);
|
||||
const pendingFields = state.pendingChanges.fields || {};
|
||||
delete pendingChanges.fields;
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
const customFilterHandlers = {
|
||||
REMOVE: 'remove',
|
||||
SAVE: 'save'
|
||||
};
|
||||
|
||||
export default customFilterHandlers;
|
|
@ -26,6 +26,10 @@ function MissingRow(props) {
|
|||
onSelectedChange
|
||||
} = props;
|
||||
|
||||
if (!artist) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"@fortawesome/free-regular-svg-icons": "5.3.1",
|
||||
"@fortawesome/free-solid-svg-icons": "5.3.1",
|
||||
"@fortawesome/react-fontawesome": "0.1.3",
|
||||
"@sentry/browser": "4.0.4",
|
||||
"autoprefixer": "9.1.5",
|
||||
"babel-core": "6.26.3",
|
||||
"babel-eslint": "9.0.0",
|
||||
|
@ -73,17 +74,15 @@
|
|||
"postcss-simple-vars": "5.0.1",
|
||||
"prop-types": "15.6.2",
|
||||
"qs": "6.5.2",
|
||||
"raven-for-redux": "1.3.1",
|
||||
"raven-js": "3.27.0",
|
||||
"react": "16.5.1",
|
||||
"react": "16.5.2",
|
||||
"react-addons-shallow-compare": "15.6.2",
|
||||
"react-async-script": "1.0.0",
|
||||
"react-autosuggest": "9.4.1",
|
||||
"react-autosuggest": "9.4.2",
|
||||
"react-custom-scrollbars": "4.2.1",
|
||||
"react-dnd": "5.0.0",
|
||||
"react-dnd-html5-backend": "5.0.1",
|
||||
"react-document-title": "2.0.3",
|
||||
"react-dom": "16.5.1",
|
||||
"react-dom": "16.5.2",
|
||||
"react-google-recaptcha": "1.0.2",
|
||||
"react-lazyload": "2.3.0",
|
||||
"react-measure": "1.4.7",
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.CustomFilters;
|
||||
using Lidarr.Http;
|
||||
|
||||
namespace Lidarr.Api.V1.CustomFilters
|
||||
{
|
||||
public class CustomFilterModule : LidarrRestModule<CustomFilterResource>
|
||||
{
|
||||
private readonly ICustomFilterService _customFilterService;
|
||||
|
||||
public CustomFilterModule(ICustomFilterService customFilterService)
|
||||
{
|
||||
_customFilterService = customFilterService;
|
||||
|
||||
GetResourceById = GetCustomFilter;
|
||||
GetResourceAll = GetCustomFilters;
|
||||
CreateResource = AddCustomFilter;
|
||||
UpdateResource = UpdateCustomFilter;
|
||||
DeleteResource = DeleteCustomResource;
|
||||
}
|
||||
|
||||
private CustomFilterResource GetCustomFilter(int id)
|
||||
{
|
||||
return _customFilterService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
private List<CustomFilterResource> GetCustomFilters()
|
||||
{
|
||||
return _customFilterService.All().ToResource();
|
||||
}
|
||||
|
||||
private int AddCustomFilter(CustomFilterResource resource)
|
||||
{
|
||||
var customFilter = _customFilterService.Add(resource.ToModel());
|
||||
|
||||
return customFilter.Id;
|
||||
}
|
||||
|
||||
private void UpdateCustomFilter(CustomFilterResource resource)
|
||||
{
|
||||
_customFilterService.Update(resource.ToModel());
|
||||
}
|
||||
|
||||
private void DeleteCustomResource(int id)
|
||||
{
|
||||
_customFilterService.Delete(id);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.CustomFilters;
|
||||
using Lidarr.Http.REST;
|
||||
|
||||
namespace Lidarr.Api.V1.CustomFilters
|
||||
{
|
||||
public class CustomFilterResource : RestResource
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Label { get; set; }
|
||||
public List<dynamic> Filters { get; set; }
|
||||
}
|
||||
|
||||
public static class CustomFilterResourceMapper
|
||||
{
|
||||
public static CustomFilterResource ToResource(this CustomFilter model)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new CustomFilterResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Type = model.Type,
|
||||
Label = model.Label,
|
||||
Filters = Json.Deserialize<List<dynamic>>(model.Filters)
|
||||
};
|
||||
}
|
||||
|
||||
public static CustomFilter ToModel(this CustomFilterResource resource)
|
||||
{
|
||||
if (resource == null) return null;
|
||||
|
||||
return new CustomFilter
|
||||
{
|
||||
Id = resource.Id,
|
||||
Type = resource.Type,
|
||||
Label = resource.Label,
|
||||
Filters = Json.ToJson(resource.Filters)
|
||||
};
|
||||
}
|
||||
|
||||
public static List<CustomFilterResource> ToResource(this IEnumerable<CustomFilter> filters)
|
||||
{
|
||||
return filters.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -97,6 +97,8 @@
|
|||
<Compile Include="Commands\CommandResource.cs" />
|
||||
<Compile Include="Config\MetadataProviderConfigModule.cs" />
|
||||
<Compile Include="Config\MetadataProviderConfigResource.cs" />
|
||||
<Compile Include="CustomFilters\CustomFilterModule.cs" />
|
||||
<Compile Include="CustomFilters\CustomFilterResource.cs" />
|
||||
<Compile Include="ImportLists\ImportListModule.cs" />
|
||||
<Compile Include="ImportLists\ImportListResource.cs" />
|
||||
<Compile Include="Profiles\Metadata\MetadataProfileModule.cs" />
|
||||
|
|
|
@ -54,8 +54,8 @@
|
|||
<HintPath>..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\Org.Mentalis.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="SharpRaven, Version=2.2.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\SharpRaven.2.2.0\lib\net45\SharpRaven.dll</HintPath>
|
||||
<Reference Include="SharpRaven, Version=2.4.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\SharpRaven.2.4.0\lib\net45\SharpRaven.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="SocksWebProxy, Version=1.3.4.0, Culture=neutral, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\SocksWebProxy.dll</HintPath>
|
||||
|
@ -66,6 +66,7 @@
|
|||
<Reference Include="System.Configuration.Install" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Xml" />
|
||||
|
|
|
@ -4,5 +4,5 @@
|
|||
<package id="ICSharpCode.SharpZipLib.Patched" version="0.86.5" targetFramework="net461" />
|
||||
<package id="Newtonsoft.Json" version="11.0.2" targetFramework="net461" />
|
||||
<package id="NLog" version="4.5.4" targetFramework="net461" />
|
||||
<package id="SharpRaven" version="2.2.0" targetFramework="net461" />
|
||||
<package id="SharpRaven" version="2.4.0" targetFramework="net461" />
|
||||
</packages>
|
|
@ -0,0 +1,11 @@
|
|||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.CustomFilters
|
||||
{
|
||||
public class CustomFilter : ModelBase
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Label { get; set; }
|
||||
public string Filters { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.CustomFilters
|
||||
{
|
||||
public interface ICustomFilterRepository : IBasicRepository<CustomFilter>
|
||||
{
|
||||
}
|
||||
|
||||
public class CustomFilterRepository : BasicRepository<CustomFilter>, ICustomFilterRepository
|
||||
{
|
||||
public CustomFilterRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue