New: Server Side UI Filtering, Error Boundaries (#501)

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
pull/505/head
Qstick 6 years ago committed by GitHub
parent a95191dc3b
commit 64a8d02f77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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 // you can use this if you don't care about the server code and just want to work
// with the web code. // with the web code.

@ -6,7 +6,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import EpisodeLanguage from 'Album/EpisodeLanguage'; import EpisodeLanguage from 'Album/EpisodeLanguage';
import EpisodeQuality from 'Album/EpisodeQuality'; import TrackQuality from 'Album/TrackQuality';
import ArtistNameLink from 'Artist/ArtistNameLink'; import ArtistNameLink from 'Artist/ArtistNameLink';
import BlacklistDetailsModal from './BlacklistDetailsModal'; import BlacklistDetailsModal from './BlacklistDetailsModal';
import styles from './BlacklistRow.css'; import styles from './BlacklistRow.css';
@ -103,7 +103,7 @@ class BlacklistRow extends Component {
key={name} key={name}
className={styles.quality} className={styles.quality}
> >
<EpisodeQuality <TrackQuality
quality={quality} quality={quality}
/> />
</TableRowCell> </TableRowCell>

@ -7,7 +7,7 @@ import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import AlbumTitleLink from 'Album/AlbumTitleLink'; import AlbumTitleLink from 'Album/AlbumTitleLink';
import EpisodeLanguage from 'Album/EpisodeLanguage'; import EpisodeLanguage from 'Album/EpisodeLanguage';
import EpisodeQuality from 'Album/EpisodeQuality'; import TrackQuality from 'Album/TrackQuality';
import ArtistNameLink from 'Artist/ArtistNameLink'; import ArtistNameLink from 'Artist/ArtistNameLink';
import HistoryEventTypeCell from './HistoryEventTypeCell'; import HistoryEventTypeCell from './HistoryEventTypeCell';
import HistoryDetailsModal from './Details/HistoryDetailsModal'; import HistoryDetailsModal from './Details/HistoryDetailsModal';
@ -142,7 +142,7 @@ class HistoryRow extends Component {
if (name === 'quality') { if (name === 'quality') {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
<EpisodeQuality <TrackQuality
quality={quality} quality={quality}
isCutoffMet={qualityCutoffNotMet} isCutoffMet={qualityCutoffNotMet}
/> />

@ -10,7 +10,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import AlbumTitleLink from 'Album/AlbumTitleLink'; import AlbumTitleLink from 'Album/AlbumTitleLink';
import EpisodeQuality from 'Album/EpisodeQuality'; import TrackQuality from 'Album/TrackQuality';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import ArtistNameLink from 'Artist/ArtistNameLink'; import ArtistNameLink from 'Artist/ArtistNameLink';
import QueueStatusCell from './QueueStatusCell'; import QueueStatusCell from './QueueStatusCell';
@ -177,7 +177,7 @@ class QueueRow extends Component {
if (name === 'quality') { if (name === 'quality') {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
<EpisodeQuality <TrackQuality
quality={quality} quality={quality}
/> />
</TableRowCell> </TableRowCell>

@ -4,7 +4,7 @@ import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import InteractiveAlbumSearchModal from './Search/InteractiveAlbumSearchModal'; import InteractiveSearchModal from 'InteractiveSearch/InteractiveSearchModal';
import styles from './AlbumSearchCell.css'; import styles from './AlbumSearchCell.css';
class AlbumSearchCell extends Component { class AlbumSearchCell extends Component {
@ -55,7 +55,7 @@ class AlbumSearchCell extends Component {
onPress={this.onManualSearchPress} onPress={this.onManualSearchPress}
/> />
<InteractiveAlbumSearchModal <InteractiveSearchModal
isOpen={this.state.isDetailsModalOpen} isOpen={this.state.isDetailsModalOpen}
albumId={albumId} albumId={albumId}
onModalClose={this.onDetailsModalClose} onModalClose={this.onDetailsModalClose}

@ -22,7 +22,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton'; import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import AlbumDetailsMediumConnector from './AlbumDetailsMediumConnector'; import AlbumDetailsMediumConnector from './AlbumDetailsMediumConnector';
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal'; import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
import InteractiveAlbumSearchModal from 'Album/Search/InteractiveAlbumSearchModal'; import InteractiveSearchModal from 'InteractiveSearch/InteractiveSearchModal';
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal'; import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
import styles from './AlbumDetails.css'; import styles from './AlbumDetails.css';
@ -415,7 +415,7 @@ class AlbumDetails extends Component {
onModalClose={this.onManageTracksModalClose} onModalClose={this.onManageTracksModalClose}
/> />
<InteractiveAlbumSearchModal <InteractiveSearchModal
isOpen={isInteractiveSearchModalOpen} isOpen={isInteractiveSearchModalOpen}
albumId={id} albumId={id}
onModalClose={this.onInteractiveSearchModalClose} onModalClose={this.onInteractiveSearchModalClose}

@ -5,7 +5,7 @@ import { icons, kinds, sizes } from 'Helpers/Props';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import ProgressBar from 'Components/ProgressBar'; import ProgressBar from 'Components/ProgressBar';
import QueueDetails from 'Activity/Queue/QueueDetails'; import QueueDetails from 'Activity/Queue/QueueDetails';
import EpisodeQuality from './EpisodeQuality'; import TrackQuality from './TrackQuality';
import styles from './EpisodeStatus.css'; import styles from './EpisodeStatus.css';
function EpisodeStatus(props) { function EpisodeStatus(props) {
@ -63,7 +63,7 @@ function EpisodeStatus(props) {
return ( return (
<div className={styles.center}> <div className={styles.center}>
<EpisodeQuality <TrackQuality
quality={quality} quality={quality}
size={trackFile.size} size={trackFile.size}
isCutoffNotMet={isCutoffNotMet} isCutoffNotMet={isCutoffNotMet}

@ -22,7 +22,7 @@ function getTooltip(title, quality, size) {
return title; return title;
} }
function EpisodeQuality(props) { function TrackQuality(props) {
const { const {
className, className,
title, title,
@ -42,7 +42,7 @@ function EpisodeQuality(props) {
); );
} }
EpisodeQuality.propTypes = { TrackQuality.propTypes = {
className: PropTypes.string, className: PropTypes.string,
title: PropTypes.string, title: PropTypes.string,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
@ -50,8 +50,8 @@ EpisodeQuality.propTypes = {
isCutoffNotMet: PropTypes.bool isCutoffNotMet: PropTypes.bool
}; };
EpisodeQuality.defaultProps = { TrackQuality.defaultProps = {
title: '' title: ''
}; };
export default EpisodeQuality; export default TrackQuality;

@ -13,6 +13,7 @@ import FilterMenu from 'Components/Menu/FilterMenu';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import NoArtist from 'Artist/NoArtist'; import NoArtist from 'Artist/NoArtist';
import AlbumStudioFilterModalConnector from './AlbumStudioFilterModalConnector';
import AlbumStudioRowConnector from './AlbumStudioRowConnector'; import AlbumStudioRowConnector from './AlbumStudioRowConnector';
import AlbumStudioFooter from './AlbumStudioFooter'; import AlbumStudioFooter from './AlbumStudioFooter';
@ -130,6 +131,7 @@ class AlbumStudio extends Component {
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
filters={filters} filters={filters}
customFilters={customFilters} customFilters={customFilters}
filterModalConnectorComponent={AlbumStudioFilterModalConnector}
onFilterSelect={onFilterSelect} onFilterSelect={onFilterSelect}
/> />
</PageToolbarSection> </PageToolbarSection>
@ -202,7 +204,7 @@ AlbumStudio.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string, sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all), sortDirection: PropTypes.oneOf(sortDirections.all),
selectedFilterKey: PropTypes.string.isRequired, selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool.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( return createSelector(
createArtistSelector(), createArtistSelector(),
createTrackFileSelector(), createTrackFileSelector(),
(artist, trackFile) => { (artist = {}, trackFile) => {
return { return {
foreignArtistId: artist.foreignArtistId, foreignArtistId: artist.foreignArtistId,
artistMonitored: artist.monitored, artistMonitored: artist.monitored,

@ -11,6 +11,7 @@ import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Label from 'Components/Label'; import Label from 'Components/Label';
import Measure from 'Components/Measure';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';

@ -273,7 +273,7 @@ ArtistEditor.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string, sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all), sortDirection: PropTypes.oneOf(sortDirections.all),
selectedFilterKey: PropTypes.string.isRequired, selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import * as artistEditorActions from 'Store/Actions/artistEditorActions'; import { setArtistEditorFilter } from 'Store/Actions/artistEditorActions';
import FilterModal from 'Components/Filter/FilterModal'; import FilterModal from 'Components/Filter/FilterModal';
function createMapStateToProps() { function createMapStateToProps() {
@ -10,22 +10,15 @@ function createMapStateToProps() {
(sectionItems, filterBuilderProps) => { (sectionItems, filterBuilderProps) => {
return { return {
sectionItems, sectionItems,
filterBuilderProps filterBuilderProps,
customFilterType: 'artistEditor'
}; };
} }
); );
} }
function createMapDispatchToProps(dispatch, props) { const mapDispatchToProps = {
return { dispatchSetFilter: setArtistEditorFilter
onRemoveCustomFilterPress(payload) { };
dispatch(artistEditorActions.removeArtistEditorCustomFilter(payload));
},
onSaveCustomFilterPress(payload) { export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
dispatch(artistEditorActions.saveArtistEditorCustomFilter(payload));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);

@ -119,4 +119,8 @@ ArtistEditorRow.propTypes = {
onSelectedChange: PropTypes.func.isRequired onSelectedChange: PropTypes.func.isRequired
}; };
ArtistEditorRow.defaultProps = {
tags: []
};
export default ArtistEditorRow; export default ArtistEditorRow;

@ -9,7 +9,7 @@ import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import EpisodeLanguage from 'Album/EpisodeLanguage'; import EpisodeLanguage from 'Album/EpisodeLanguage';
import EpisodeQuality from 'Album/EpisodeQuality'; import TrackQuality from 'Album/TrackQuality';
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector'; import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell'; import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
import styles from './ArtistHistoryRow.css'; import styles from './ArtistHistoryRow.css';
@ -100,7 +100,7 @@ class ArtistHistoryRow extends Component {
</TableRowCell> </TableRowCell>
<TableRowCell> <TableRowCell>
<EpisodeQuality <TrackQuality
quality={quality} quality={quality}
isCutoffNotMet={qualityCutoffNotMet} isCutoffNotMet={qualityCutoffNotMet}
/> />

@ -215,7 +215,7 @@ class ArtistIndex extends Component {
} = this.state; } = this.state;
const ViewComponent = getViewComponent(view); const ViewComponent = getViewComponent(view);
const isLoaded = !error && isPopulated && !!items.length && contentBody; const isLoaded = !!(!error && isPopulated && items.length && contentBody);
const hasNoArtist = !totalItems; const hasNoArtist = !totalItems;
return ( return (
@ -382,7 +382,7 @@ ArtistIndex.propTypes = {
error: PropTypes.object, error: PropTypes.object,
totalItems: PropTypes.number.isRequired, totalItems: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).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, filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string, sortKey: PropTypes.string,

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import * as artistIndexActions from 'Store/Actions/artistIndexActions'; import { setArtistFilter } from 'Store/Actions/artistIndexActions';
import FilterModal from 'Components/Filter/FilterModal'; import FilterModal from 'Components/Filter/FilterModal';
function createMapStateToProps() { function createMapStateToProps() {
@ -10,22 +10,15 @@ function createMapStateToProps() {
(sectionItems, filterBuilderProps) => { (sectionItems, filterBuilderProps) => {
return { return {
sectionItems, sectionItems,
filterBuilderProps filterBuilderProps,
customFilterType: 'artistIndex'
}; };
} }
); );
} }
function createMapDispatchToProps(dispatch, props) { const mapDispatchToProps = {
return { dispatchSetFilter: setArtistFilter
onRemoveCustomFilterPress(payload) { };
dispatch(artistIndexActions.removeArtistCustomFilter(payload));
},
onSaveCustomFilterPress(payload) { export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
dispatch(artistIndexActions.saveArtistCustomFilter(payload));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(FilterModal);

@ -1,12 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Measure from 'react-measure';
import { Grid, WindowScroller } from 'react-virtualized'; import { Grid, WindowScroller } from 'react-virtualized';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import { sortDirections } from 'Helpers/Props'; import { sortDirections } from 'Helpers/Props';
import Measure from 'Components/Measure';
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
import ArtistIndexBanner from './ArtistIndexBanner'; import ArtistIndexBanner from './ArtistIndexBanner';
import styles from './ArtistIndexBanners.css'; import styles from './ArtistIndexBanners.css';

@ -27,7 +27,7 @@ function ArtistIndexFilterMenu(props) {
} }
ArtistIndexFilterMenu.propTypes = { ArtistIndexFilterMenu.propTypes = {
selectedFilterKey: PropTypes.string.isRequired, selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired, isDisabled: PropTypes.bool.isRequired,

@ -2,12 +2,12 @@ import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Measure from 'react-measure';
import { Grid, WindowScroller } from 'react-virtualized'; import { Grid, WindowScroller } from 'react-virtualized';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import { sortDirections } from 'Helpers/Props'; import { sortDirections } from 'Helpers/Props';
import Measure from 'Components/Measure';
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
import ArtistIndexOverview from './ArtistIndexOverview'; import ArtistIndexOverview from './ArtistIndexOverview';
import styles from './ArtistIndexOverviews.css'; import styles from './ArtistIndexOverviews.css';

@ -1,12 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Measure from 'react-measure';
import { Grid, WindowScroller } from 'react-virtualized'; import { Grid, WindowScroller } from 'react-virtualized';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import { sortDirections } from 'Helpers/Props'; import { sortDirections } from 'Helpers/Props';
import Measure from 'Components/Measure';
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector'; import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
import ArtistIndexPoster from './ArtistIndexPoster'; import ArtistIndexPoster from './ArtistIndexPoster';
import styles from './ArtistIndexPosters.css'; import styles from './ArtistIndexPosters.css';

@ -1,8 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Measure from 'react-measure';
import { align, icons } from 'Helpers/Props'; import { align, icons } from 'Helpers/Props';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import Measure from 'Components/Measure';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';

@ -56,6 +56,10 @@ class CalendarEvent extends Component {
colorImpairedMode colorImpairedMode
} = this.props; } = this.props;
if (!artist) {
return null;
}
const startTime = moment(releaseDate); const startTime = moment(releaseDate);
// const endTime = startTime.add(artist.runtime, 'minutes'); // const endTime = startTime.add(artist.runtime, 'minutes');
const downloading = !!(queueItem || grabbed); 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, directories,
files, files,
paths: filteredPaths, 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 { inputTypes } from 'Helpers/Props';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody'; 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 // Listeners
@ -70,9 +93,9 @@ class FilterBuilderModalContent extends Component {
onSaveFilterPress = () => { onSaveFilterPress = () => {
const { const {
customFilterKey: key, id,
onSaveCustomFilterPress, customFilterType,
onModalClose onSaveCustomFilterPress
} = this.props; } = this.props;
const { const {
@ -92,8 +115,12 @@ class FilterBuilderModalContent extends Component {
return; return;
} }
onSaveCustomFilterPress({ key, label, filters }); onSaveCustomFilterPress({
onModalClose(); id,
type: customFilterType,
label,
filters
});
} }
// //
@ -103,6 +130,8 @@ class FilterBuilderModalContent extends Component {
const { const {
sectionItems, sectionItems,
filterBuilderProps, filterBuilderProps,
isSaving,
saveError,
onModalClose onModalClose
} = this.props; } = this.props;
@ -161,17 +190,17 @@ class FilterBuilderModalContent extends Component {
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
<Button <Button onPress={onModalClose}>
onPress={onModalClose}
>
Cancel Cancel
</Button> </Button>
<Button <SpinnerErrorButton
isSpinning={isSaving}
error={saveError}
onPress={this.onSaveFilterPress} onPress={this.onSaveFilterPress}
> >
Apply Save
</Button> </SpinnerErrorButton>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
); );
@ -179,13 +208,18 @@ class FilterBuilderModalContent extends Component {
} }
FilterBuilderModalContent.propTypes = { FilterBuilderModalContent.propTypes = {
customFilterKey: PropTypes.string, id: PropTypes.number,
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
customFilterType: PropTypes.string.isRequired,
sectionItems: PropTypes.arrayOf(PropTypes.object).isRequired, sectionItems: PropTypes.arrayOf(PropTypes.object).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired,
filterBuilderProps: 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, onSaveCustomFilterPress: PropTypes.func.isRequired,
dispatchSetFilter: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

@ -1,28 +1,42 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { saveCustomFilter, deleteCustomFilter } from 'Store/Actions/customFilterActions';
import FilterBuilderModalContent from './FilterBuilderModalContent'; import FilterBuilderModalContent from './FilterBuilderModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state, { customFilters }) => customFilters, (state, { customFilters }) => customFilters,
(state, { customFilterKey }) => customFilterKey, (state, { id }) => id,
(customFilters, customFilterKey) => { (state) => state.customFilters.isSaving,
if (customFilterKey) { (state) => state.customFilters.saveError,
const customFilter = customFilters.find((c) => c.key === customFilterKey); (customFilters, id, isSaving, saveError) => {
if (id) {
const customFilter = customFilters.find((c) => c.id === id);
return { return {
customFilterKey: customFilter.key, id: customFilter.id,
label: customFilter.label, label: customFilter.label,
filters: customFilter.filters filters: customFilter.filters,
customFilters,
isSaving,
saveError
}; };
} }
return { return {
label: '', 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 PropTypes from 'prop-types';
import React, { Component } from 'react'; 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 TagInput, { tagShape } from 'Components/Form/TagInput';
import FilterBuilderRowValueTag from './FilterBuilderRowValueTag'; import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
export const NAME = 'value'; 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 { class FilterBuilderRowValue extends Component {
// //
@ -18,17 +71,15 @@ class FilterBuilderRowValue extends Component {
onChange onChange
} = this.props; } = this.props;
let id = tag.id; let value = tag.id;
if (id == null) { if (value == null) {
id = selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ? value = getValue(tag.name, selectedFilterBuilderProp);
parseInt(tag.name) :
tag.name;
} }
onChange({ onChange({
name: NAME, name: NAME,
value: [...filterValue, id] value: [...filterValue, value]
}); });
} }
@ -52,6 +103,7 @@ class FilterBuilderRowValue extends Component {
render() { render() {
const { const {
filterValue, filterValue,
selectedFilterBuilderProp,
tagList tagList
} = this.props; } = this.props;
@ -68,7 +120,7 @@ class FilterBuilderRowValue extends Component {
} }
return { return {
id, id,
name: id name: getTagDisplayValue(id, selectedFilterBuilderProp)
}; };
}); });

@ -2,29 +2,70 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import styles from './CustomFilter.css'; import styles from './CustomFilter.css';
class CustomFilter extends Component { 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 // Listeners
onEditPress = () => { onEditPress = () => {
const { const {
customFilterKey, id,
onEditPress onEditPress
} = this.props; } = this.props;
onEditPress(customFilterKey); onEditPress(id);
} }
onRemovePress = () => { onRemovePress = () => {
const { const {
customFilterKey, id,
onRemovePress dispatchDeleteCustomFilter
} = this.props; } = this.props;
onRemovePress({ key: customFilterKey }); this.setState({ isDeleting: true }, () => {
dispatchDeleteCustomFilter({ id });
});
} }
// //
@ -47,8 +88,9 @@ class CustomFilter extends Component {
onPress={this.onEditPress} onPress={this.onEditPress}
/> />
<IconButton <SpinnerIconButton
name={icons.REMOVE} name={icons.REMOVE}
isSpinning={this.state.isDeleting}
onPress={this.onRemovePress} onPress={this.onRemovePress}
/> />
</div> </div>
@ -58,10 +100,14 @@ class CustomFilter extends Component {
} }
CustomFilter.propTypes = { CustomFilter.propTypes = {
customFilterKey: PropTypes.string.isRequired, id: PropTypes.number.isRequired,
label: PropTypes.string.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, onEditPress: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired dispatchDeleteCustomFilter: PropTypes.func.isRequired
}; };
export default CustomFilter; export default CustomFilter;

@ -10,9 +10,13 @@ import styles from './CustomFiltersModalContent.css';
function CustomFiltersModalContent(props) { function CustomFiltersModalContent(props) {
const { const {
selectedFilterKey,
customFilters, customFilters,
isDeleting,
deleteError,
dispatchDeleteCustomFilter,
dispatchSetFilter,
onAddCustomFilter, onAddCustomFilter,
onRemoveCustomFilterPress,
onEditCustomFilter, onEditCustomFilter,
onModalClose onModalClose
} = props; } = props;
@ -29,10 +33,14 @@ function CustomFiltersModalContent(props) {
return ( return (
<CustomFilter <CustomFilter
key={index} key={index}
customFilterKey={customFilter.key} id={customFilter.id}
label={customFilter.label} label={customFilter.label}
filters={customFilter.filters} filters={customFilter.filters}
onRemovePress={onRemoveCustomFilterPress} selectedFilterKey={selectedFilterKey}
isDeleting={isDeleting}
deleteError={deleteError}
dispatchSetFilter={dispatchSetFilter}
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
onEditPress={onEditCustomFilter} onEditPress={onEditCustomFilter}
/> />
); );
@ -58,9 +66,13 @@ function CustomFiltersModalContent(props) {
} }
CustomFiltersModalContent.propTypes = { CustomFiltersModalContent.propTypes = {
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).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, onAddCustomFilter: PropTypes.func.isRequired,
onRemoveCustomFilterPress: PropTypes.func.isRequired,
onEditCustomFilter: PropTypes.func.isRequired, onEditCustomFilter: PropTypes.func.isRequired,
onModalClose: 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 React, { Component } from 'react';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import FilterBuilderModalContentConnector from './Builder/FilterBuilderModalContentConnector'; import FilterBuilderModalContentConnector from './Builder/FilterBuilderModalContentConnector';
import CustomFiltersModalContent from './CustomFilters/CustomFiltersModalContent'; import CustomFiltersModalContentConnector from './CustomFilters/CustomFiltersModalContentConnector';
class FilterModal extends Component { class FilterModal extends Component {
@ -14,7 +14,7 @@ class FilterModal extends Component {
this.state = { this.state = {
filterBuilder: !props.customFilters.length, filterBuilder: !props.customFilters.length,
customFilterKey: null id: null
}; };
} }
@ -27,17 +27,17 @@ class FilterModal extends Component {
}); });
} }
onEditCustomFilter = (customFilterKey) => { onEditCustomFilter = (id) => {
this.setState({ this.setState({
filterBuilder: true, filterBuilder: true,
customFilterKey id
}); });
} }
onModalClose = () => { onModalClose = () => {
this.setState({ this.setState({
filterBuilder: false, filterBuilder: false,
customFilterKey: null id: null
}, () => { }, () => {
this.props.onModalClose(); this.props.onModalClose();
}); });
@ -54,7 +54,7 @@ class FilterModal extends Component {
const { const {
filterBuilder, filterBuilder,
customFilterKey id
} = this.state; } = this.state;
return ( return (
@ -66,10 +66,10 @@ class FilterModal extends Component {
filterBuilder ? filterBuilder ?
<FilterBuilderModalContentConnector <FilterBuilderModalContentConnector
{...otherProps} {...otherProps}
customFilterKey={customFilterKey} id={id}
onModalClose={this.onModalClose} onModalClose={this.onModalClose}
/> : /> :
<CustomFiltersModalContent <CustomFiltersModalContentConnector
{...otherProps} {...otherProps}
onAddCustomFilter={this.onAddCustomFilter} onAddCustomFilter={this.onAddCustomFilter}
onEditCustomFilter={this.onEditCustomFilter} onEditCustomFilter={this.onEditCustomFilter}

@ -2,7 +2,6 @@ import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Measure from 'react-measure';
import TetherComponent from 'react-tether'; import TetherComponent from 'react-tether';
import classNames from 'classnames'; import classNames from 'classnames';
import isMobileUtil from 'Utilities/isMobile'; import isMobileUtil from 'Utilities/isMobile';
@ -10,6 +9,7 @@ import * as keyCodes from 'Utilities/Constants/keyCodes';
import { icons, scrollDirections } from 'Helpers/Props'; import { icons, scrollDirections } from 'Helpers/Props';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import Measure from 'Components/Measure';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import Scroller from 'Components/Scroller/Scroller'; import Scroller from 'Components/Scroller/Scroller';

@ -62,13 +62,15 @@ class PathInput extends Component {
event.preventDefault(); event.preventDefault();
const path = this.props.paths[0]; const path = this.props.paths[0];
this.props.onChange({ if (path) {
name: this.props.name, this.props.onChange({
value: path.path name: this.props.name,
}); value: path.path
});
if (path.type !== 'file') {
this.props.onFetchPaths(path.path); if (path.type !== 'file') {
this.props.onFetchPaths(path.path);
}
} }
} }
} }

@ -39,6 +39,10 @@ class TagInput extends Component {
this._autosuggestRef = null; this._autosuggestRef = null;
} }
componentWillUnmount() {
this.addTag.cancel();
}
// //
// Control // 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, customFilters,
buttonComponent: ButtonComponent, buttonComponent: ButtonComponent,
filterModalConnectorComponent: FilterModalConnectorComponent, filterModalConnectorComponent: FilterModalConnectorComponent,
filterModalConnectorComponentProps,
onFilterSelect, onFilterSelect,
...otherProps ...otherProps
} = this.props; } = this.props;
@ -74,6 +75,7 @@ class FilterMenu extends Component {
{ {
showCustomFilters && showCustomFilters &&
<FilterModalConnectorComponent <FilterModalConnectorComponent
{...filterModalConnectorComponentProps}
isOpen={this.state.isFilterModalOpen} isOpen={this.state.isFilterModalOpen}
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
filters={filters} filters={filters}
@ -90,11 +92,12 @@ class FilterMenu extends Component {
FilterMenu.propTypes = { FilterMenu.propTypes = {
className: PropTypes.string, className: PropTypes.string,
isDisabled: PropTypes.bool.isRequired, isDisabled: PropTypes.bool.isRequired,
selectedFilterKey: PropTypes.string.isRequired, selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
buttonComponent: PropTypes.func.isRequired, buttonComponent: PropTypes.func.isRequired,
filterModalConnectorComponent: PropTypes.func, filterModalConnectorComponent: PropTypes.func,
filterModalConnectorComponentProps: PropTypes.object,
onFilterSelect: PropTypes.func.isRequired onFilterSelect: PropTypes.func.isRequired
}; };

@ -42,8 +42,8 @@ class FilterMenuContent extends Component {
customFilters.map((filter) => { customFilters.map((filter) => {
return ( return (
<FilterMenuItem <FilterMenuItem
key={filter.key} key={filter.id}
filterKey={filter.key} filterKey={filter.id}
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect} onPress={onFilterSelect}
> >
@ -70,7 +70,7 @@ class FilterMenuContent extends Component {
} }
FilterMenuContent.propTypes = { FilterMenuContent.propTypes = {
selectedFilterKey: PropTypes.string.isRequired, selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
showCustomFilters: PropTypes.bool.isRequired, showCustomFilters: PropTypes.bool.isRequired,

@ -37,8 +37,8 @@ class FilterMenuItem extends Component {
} }
FilterMenuItem.propTypes = { FilterMenuItem.propTypes = {
filterKey: PropTypes.string.isRequired, filterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
selectedFilterKey: PropTypes.string.isRequired, selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
onPress: PropTypes.func.isRequired onPress: PropTypes.func.isRequired
}; };

@ -6,6 +6,8 @@ import elementClass from 'element-class';
import getUniqueElememtId from 'Utilities/getUniqueElementId'; import getUniqueElememtId from 'Utilities/getUniqueElementId';
import * as keyCodes from 'Utilities/Constants/keyCodes'; import * as keyCodes from 'Utilities/Constants/keyCodes';
import { sizes } from 'Helpers/Props'; import { sizes } from 'Helpers/Props';
import ErrorBoundary from 'Components/Error/ErrorBoundary';
import ModalError from './ModalError';
import styles from './Modal.css'; import styles from './Modal.css';
const openModals = []; const openModals = [];
@ -153,7 +155,8 @@ class Modal extends Component {
backdropClassName, backdropClassName,
size, size,
children, children,
isOpen isOpen,
onModalClose
} = this.props; } = this.props;
if (!isOpen) { if (!isOpen) {
@ -177,7 +180,12 @@ class Modal extends Component {
)} )}
style={style} style={style}
> >
{children} <ErrorBoundary
errorComponent={ModalError}
onModalClose={onModalClose}
>
{children}
</ErrorBoundary>
</div> </div>
</div> </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, version,
isLocalStorageSupported, isLocalStorageSupported,
artistError, artistError,
customFiltersError,
tagsError, tagsError,
qualityProfilesError, qualityProfilesError,
languageProfilesError,
metadataProfilesError,
uiSettingsError uiSettingsError
} = props; } = 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.'; errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
} else if (artistError) { } else if (artistError) {
errorMessage = getErrorMessage(artistError, 'Failed to load artist from API'); 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) { } else if (tagsError) {
errorMessage = getErrorMessage(artistError, 'Failed to load artist from API'); errorMessage = getErrorMessage(tagsError, 'Failed to load tags from API');
} else if (qualityProfilesError) { } else if (qualityProfilesError) {
errorMessage = getErrorMessage(qualityProfilesError, 'Failed to load quality profiles from API'); 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) { } else if (uiSettingsError) {
errorMessage = getErrorMessage(uiSettingsError, 'Failed to load UI settings from API'); errorMessage = getErrorMessage(uiSettingsError, 'Failed to load UI settings from API');
} }
@ -44,8 +53,11 @@ ErrorPage.propTypes = {
version: PropTypes.string.isRequired, version: PropTypes.string.isRequired,
isLocalStorageSupported: PropTypes.bool.isRequired, isLocalStorageSupported: PropTypes.bool.isRequired,
artistError: PropTypes.object, artistError: PropTypes.object,
customFiltersError: PropTypes.object,
tagsError: PropTypes.object, tagsError: PropTypes.object,
qualityProfilesError: PropTypes.object, qualityProfilesError: PropTypes.object,
languageProfilesError: PropTypes.object,
metadataProfilesError: PropTypes.object,
uiSettingsError: PropTypes.object uiSettingsError: PropTypes.object
}; };

@ -6,6 +6,7 @@ import { withRouter } from 'react-router-dom';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchArtist } from 'Store/Actions/artistActions'; import { fetchArtist } from 'Store/Actions/artistActions';
import { fetchTags } from 'Store/Actions/tagActions'; import { fetchTags } from 'Store/Actions/tagActions';
import { fetchQualityProfiles, fetchLanguageProfiles, fetchMetadataProfiles, fetchUISettings, fetchImportLists } from 'Store/Actions/settingsActions'; import { fetchQualityProfiles, fetchLanguageProfiles, fetchMetadataProfiles, fetchUISettings, fetchImportLists } from 'Store/Actions/settingsActions';
@ -30,13 +31,15 @@ function testLocalStorage() {
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.artist, (state) => state.artist,
(state) => state.customFilters,
(state) => state.tags, (state) => state.tags,
(state) => state.settings, (state) => state.settings,
(state) => state.app, (state) => state.app,
createDimensionsSelector(), createDimensionsSelector(),
(artist, tags, settings, app, dimensions) => { (artist, customFilters, tags, settings, app, dimensions) => {
const isPopulated = ( const isPopulated = (
artist.isPopulated && artist.isPopulated &&
customFilters.isPopulated &&
tags.isPopulated && tags.isPopulated &&
settings.qualityProfiles.isPopulated && settings.qualityProfiles.isPopulated &&
settings.languageProfiles.isPopulated && settings.languageProfiles.isPopulated &&
@ -47,6 +50,7 @@ function createMapStateToProps() {
const hasError = !!( const hasError = !!(
artist.error || artist.error ||
customFilters.error ||
tags.error || tags.error ||
settings.qualityProfiles.error || settings.qualityProfiles.error ||
settings.languageProfiles.error || settings.languageProfiles.error ||
@ -59,6 +63,7 @@ function createMapStateToProps() {
isPopulated, isPopulated,
hasError, hasError,
artistError: artist.error, artistError: artist.error,
customFiltersError: tags.error,
tagsError: tags.error, tagsError: tags.error,
qualityProfilesError: settings.qualityProfiles.error, qualityProfilesError: settings.qualityProfiles.error,
languageProfilesError: settings.languageProfiles.error, languageProfilesError: settings.languageProfiles.error,
@ -80,6 +85,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchArtist() { dispatchFetchArtist() {
dispatch(fetchArtist()); dispatch(fetchArtist());
}, },
dispatchFetchCustomFilters() {
dispatch(fetchCustomFilters());
},
dispatchFetchTags() { dispatchFetchTags() {
dispatch(fetchTags()); dispatch(fetchTags());
}, },
@ -126,6 +134,7 @@ class PageConnector extends Component {
componentDidMount() { componentDidMount() {
if (!this.props.isPopulated) { if (!this.props.isPopulated) {
this.props.dispatchFetchArtist(); this.props.dispatchFetchArtist();
this.props.dispatchFetchCustomFilters();
this.props.dispatchFetchTags(); this.props.dispatchFetchTags();
this.props.dispatchFetchQualityProfiles(); this.props.dispatchFetchQualityProfiles();
this.props.dispatchFetchLanguageProfiles(); this.props.dispatchFetchLanguageProfiles();
@ -190,6 +199,7 @@ PageConnector.propTypes = {
hasError: PropTypes.bool.isRequired, hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired, isSidebarVisible: PropTypes.bool.isRequired,
dispatchFetchArtist: PropTypes.func.isRequired, dispatchFetchArtist: PropTypes.func.isRequired,
dispatchFetchCustomFilters: PropTypes.func.isRequired,
dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchQualityProfiles: PropTypes.func.isRequired, dispatchFetchQualityProfiles: PropTypes.func.isRequired,
dispatchFetchLanguageProfiles: PropTypes.func.isRequired, dispatchFetchLanguageProfiles: PropTypes.func.isRequired,

@ -1,6 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import DocumentTitle from 'react-document-title'; import DocumentTitle from 'react-document-title';
import ErrorBoundary from 'Components/Error/ErrorBoundary';
import PageContentError from './PageContentError';
import styles from './PageContent.css'; import styles from './PageContent.css';
function PageContent(props) { function PageContent(props) {
@ -11,11 +13,13 @@ function PageContent(props) {
} = props; } = props;
return ( return (
<DocumentTitle title={title ? `${title} - Lidarr` : 'Lidarr'}> <ErrorBoundary errorComponent={PageContentError}>
<div className={className}> <DocumentTitle title={title ? `${title} - Lidarr` : 'Lidarr'}>
{children} <div className={className}>
</div> {children}
</DocumentTitle> </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 _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Measure from 'react-measure';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import Measure from 'Components/Measure';
import PageJumpBarItem from './PageJumpBarItem'; import PageJumpBarItem from './PageJumpBarItem';
import styles from './PageJumpBar.css'; import styles from './PageJumpBar.css';

@ -1,11 +1,11 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Measure from 'react-measure';
import classNames from 'classnames'; import classNames from 'classnames';
import { forEach } from 'Helpers/elementChildren'; import { forEach } from 'Helpers/elementChildren';
import { align, icons } from 'Helpers/Props'; import { align, icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import SpinnerIcon from 'Components/SpinnerIcon'; import SpinnerIcon from 'Components/SpinnerIcon';
import Measure from 'Components/Measure';
import Menu from 'Components/Menu/Menu'; import Menu from 'Components/Menu/Menu';
import MenuContent from 'Components/Menu/MenuContent'; import MenuContent from 'Components/Menu/MenuContent';
import MenuItem from 'Components/Menu/MenuItem'; import MenuItem from 'Components/Menu/MenuItem';

@ -1,9 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import Measure from 'react-measure';
import { WindowScroller } from 'react-virtualized'; import { WindowScroller } from 'react-virtualized';
import { scrollDirections } from 'Helpers/Props'; import { scrollDirections } from 'Helpers/Props';
import Measure from 'Components/Measure';
import Scroller from 'Components/Scroller/Scroller'; import Scroller from 'Components/Scroller/Scroller';
import VirtualTableBody from './VirtualTableBody'; import VirtualTableBody from './VirtualTableBody';
import styles from './VirtualTable.css'; 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 BOOL = 'bool';
export const BYTES = 'bytes';
export const DATE = 'date'; export const DATE = 'date';
export const DEFAULT = 'default'; export const DEFAULT = 'default';
export const INDEXER = 'indexer'; export const INDEXER = 'indexer';

@ -9,7 +9,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton'; import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import EpisodeQuality from 'Album/EpisodeQuality'; import TrackQuality from 'Album/TrackQuality';
import EpisodeLanguage from 'Album/EpisodeLanguage'; import EpisodeLanguage from 'Album/EpisodeLanguage';
import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal'; import SelectArtistModal from 'InteractiveImport/Artist/SelectArtistModal';
import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal'; import SelectAlbumModal from 'InteractiveImport/Album/SelectAlbumModal';
@ -248,7 +248,7 @@ class InteractiveImportRow extends Component {
{ {
!showQualityPlaceholder && !!quality && !showQualityPlaceholder && !!quality &&
<EpisodeQuality <TrackQuality
className={styles.label} className={styles.label}
quality={quality} quality={quality}
/> />

@ -1,6 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import * as releaseActions from 'Store/Actions/releaseActions'; import { setReleasesFilter } from 'Store/Actions/releaseActions';
import FilterModal from 'Components/Filter/FilterModal'; import FilterModal from 'Components/Filter/FilterModal';
function createMapStateToProps() { function createMapStateToProps() {
@ -10,7 +10,8 @@ function createMapStateToProps() {
(sectionItems, filterBuilderProps) => { (sectionItems, filterBuilderProps) => {
return { return {
sectionItems, sectionItems,
filterBuilderProps filterBuilderProps,
customFilterType: 'releases'
}; };
} }
); );
@ -18,12 +19,10 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) { function createMapDispatchToProps(dispatch, props) {
return { return {
onRemoveCustomFilterPress(payload) { dispatchSetFilter(payload) {
dispatch(releaseActions.removeReleasesCustomFilter(payload)); const action = setReleasesFilter;
},
onSaveCustomFilterPress(payload) { dispatch(action(payload));
dispatch(releaseActions.saveReleasesCustomFilter(payload));
} }
}; };
} }

@ -1,9 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import InteractiveAlbumSearchModalContentConnector from './InteractiveAlbumSearchModalContentConnector'; import InteractiveSearchModalContentConnector from './InteractiveSearchModalContentConnector';
function InteractiveAlbumSearchModal(props) { function InteractiveSearchModal(props) {
const { const {
isOpen, isOpen,
onModalClose, onModalClose,
@ -15,7 +15,7 @@ function InteractiveAlbumSearchModal(props) {
isOpen={isOpen} isOpen={isOpen}
onModalClose={onModalClose} onModalClose={onModalClose}
> >
<InteractiveAlbumSearchModalContentConnector <InteractiveSearchModalContentConnector
{...otherProps} {...otherProps}
onModalClose={onModalClose} onModalClose={onModalClose}
/> />
@ -23,9 +23,9 @@ function InteractiveAlbumSearchModal(props) {
); );
} }
InteractiveAlbumSearchModal.propTypes = { InteractiveSearchModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.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 Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector'; import InteractiveSearchFilterModalConnector from './InteractiveSearchFilterModalConnector';
import InteractiveAlbumSearchRow from './InteractiveAlbumSearchRow'; import InteractiveSearchRow from './InteractiveSearchRow';
import styles from './InteractiveAlbumSearchModalContent.css'; import styles from './InteractiveSearchModalContent.css';
const columns = [ const columns = [
{ {
@ -75,7 +75,7 @@ const columns = [
} }
]; ];
class InteractiveAlbumSearchModalContent extends Component { class InteractiveSearchModalContent extends Component {
// //
// Render // Render
@ -161,7 +161,7 @@ class InteractiveAlbumSearchModalContent extends Component {
{ {
items.map((item) => { items.map((item) => {
return ( return (
<InteractiveAlbumSearchRow <InteractiveSearchRow
key={item.guid} key={item.guid}
{...item} {...item}
longDateFormat={longDateFormat} longDateFormat={longDateFormat}
@ -195,7 +195,7 @@ class InteractiveAlbumSearchModalContent extends Component {
} }
} }
InteractiveAlbumSearchModalContent.propTypes = { InteractiveSearchModalContent.propTypes = {
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
@ -203,7 +203,7 @@ InteractiveAlbumSearchModalContent.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
longDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired,
timeFormat: 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, filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string, sortKey: PropTypes.string,
@ -214,4 +214,4 @@ InteractiveAlbumSearchModalContent.propTypes = {
onModalClose: PropTypes.func.isRequired 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 * as releaseActions from 'Store/Actions/releaseActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector'; import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import InteractiveAlbumSearchModalContent from './InteractiveAlbumSearchModalContent'; import InteractiveSearchModalContent from './InteractiveSearchModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
@ -51,7 +51,7 @@ function createMapDispatchToProps(dispatch, props) {
}; };
} }
class InteractiveAlbumSearchModalContentConnector extends Component { class InteractiveSearchModalContentConnector extends Component {
// //
// Lifecycle // Lifecycle
@ -81,18 +81,18 @@ class InteractiveAlbumSearchModalContentConnector extends Component {
} = this.props; } = this.props;
return ( return (
<InteractiveAlbumSearchModalContent <InteractiveSearchModalContent
{...otherProps} {...otherProps}
/> />
); );
} }
} }
InteractiveAlbumSearchModalContentConnector.propTypes = { InteractiveSearchModalContentConnector.propTypes = {
albumId: PropTypes.number, albumId: PropTypes.number,
dispatchFetchReleases: PropTypes.func.isRequired, dispatchFetchReleases: PropTypes.func.isRequired,
dispatchClearReleases: PropTypes.func.isRequired, dispatchClearReleases: PropTypes.func.isRequired,
dispatchCancelFetchReleases: 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 TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import EpisodeQuality from 'Album/EpisodeQuality'; import TrackQuality from 'Album/TrackQuality';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import Peers from './Peers'; import Peers from './Peers';
import styles from './InteractiveAlbumSearchRow.css'; import styles from './InteractiveSearchRow.css';
function getDownloadIcon(isGrabbing, isGrabbed, grabError) { function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) { if (isGrabbing) {
@ -39,7 +39,7 @@ function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
return 'Add to downloaded queue'; return 'Add to downloaded queue';
} }
class InteractiveAlbumSearchRow extends Component { class InteractiveSearchRow extends Component {
// //
// Listeners // Listeners
@ -120,7 +120,7 @@ class InteractiveAlbumSearchRow extends Component {
</TableRowCell> </TableRowCell>
<TableRowCell className={styles.quality}> <TableRowCell className={styles.quality}>
<EpisodeQuality <TrackQuality
quality={quality} quality={quality}
/> />
</TableRowCell> </TableRowCell>
@ -171,7 +171,7 @@ class InteractiveAlbumSearchRow extends Component {
} }
} }
InteractiveAlbumSearchRow.propTypes = { InteractiveSearchRow.propTypes = {
guid: PropTypes.string.isRequired, guid: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
age: PropTypes.number.isRequired, age: PropTypes.number.isRequired,
@ -196,9 +196,9 @@ InteractiveAlbumSearchRow.propTypes = {
onGrabPress: PropTypes.func.isRequired onGrabPress: PropTypes.func.isRequired
}; };
InteractiveAlbumSearchRow.defaultProps = { InteractiveSearchRow.defaultProps = {
isGrabbing: false, isGrabbing: false,
isGrabbed: false isGrabbed: false
}; };
export default InteractiveAlbumSearchRow; export default InteractiveSearchRow;

@ -1,10 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Measure from 'react-measure';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import FieldSet from 'Components/FieldSet'; import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import Measure from 'Components/Measure';
import PageSectionContent from 'Components/Page/PageSectionContent'; import PageSectionContent from 'Components/Page/PageSectionContent';
import DelayProfileDragSource from './DelayProfileDragSource'; import DelayProfileDragSource from './DelayProfileDragSource';
import DelayProfileDragPreview from './DelayProfileDragPreview'; import DelayProfileDragPreview from './DelayProfileDragPreview';

@ -1,11 +1,11 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Measure from 'react-measure';
import { inputTypes, kinds, sizes } from 'Helpers/Props'; import { inputTypes, kinds, sizes } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Measure from 'Components/Measure';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';

@ -1,12 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Measure from 'react-measure';
import { icons, kinds, sizes } from 'Helpers/Props'; import { icons, kinds, sizes } from 'Helpers/Props';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
import FormInputHelpText from 'Components/Form/FormInputHelpText'; import FormInputHelpText from 'Components/Form/FormInputHelpText';
import Measure from 'Components/Measure';
import QualityProfileItemDragSource from './QualityProfileItemDragSource'; import QualityProfileItemDragSource from './QualityProfileItemDragSource';
import QualityProfileItemDragPreview from './QualityProfileItemDragPreview'; import QualityProfileItemDragPreview from './QualityProfileItemDragPreview';
import styles from './QualityProfileItems.css'; 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) => { promise.done((data) => {
dispatch(batchActions([ dispatch(batchActions([
removeItem({ section, id }),
set({ set({
section, section,
isDeleting: false, isDeleting: false,
deleteError: null deleteError: null
}) }),
removeItem({ section, id })
])); ]));
}); });

@ -21,10 +21,11 @@ function createSaveProviderHandler(section, url, options = {}) {
const { const {
id, id,
queryParams = {} queryParams = {},
...otherPayload
} = payload; } = payload;
const saveData = getProviderState(payload, getState, section); const saveData = getProviderState({ id, ...otherPayload }, getState, section);
const ajaxOptions = { const ajaxOptions = {
url: `${url}?${$.param(queryParams, true)}`, url: `${url}?${$.param(queryParams, true)}`,

@ -2,7 +2,7 @@ import _ from 'lodash';
import $ from 'jquery'; import $ from 'jquery';
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import getMonitoringOptions from 'Utilities/Artist/getMonitoringOptions'; 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 { createThunk, handleThunks } from 'Store/thunks';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
@ -29,7 +29,55 @@ export const defaultState = {
selectedFilterKey: 'all', selectedFilterKey: 'all',
filters, filters,
filterPredicates, 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 = [ export const persistState = [

@ -103,6 +103,20 @@ export const filterPredicates = {
const predicate = filterTypePredicates[type]; const predicate = filterTypePredicates[type];
return predicate(item.ratings.value * 10, filterValue); 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 $ from 'jquery';
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions'; import { batchActions } from 'redux-batched-actions';
import customFilterHandlers from 'Utilities/customFilterHandlers';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import createCustomFilterReducers from './Creators/Reducers/createCustomFilterReducers';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
import { set, updateItem } from './baseActions'; import { set, updateItem } from './baseActions';
import { filters, filterPredicates } from './artistActions'; import { filters, filterPredicates } from './artistActions';
@ -79,8 +77,7 @@ export const defaultState = {
type: filterBuilderTypes.ARRAY, type: filterBuilderTypes.ARRAY,
valueType: filterBuilderValueTypes.TAG valueType: filterBuilderValueTypes.TAG
} }
], ]
customFilters: []
}; };
export const persistState = [ 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 SET_ARTIST_EDITOR_FILTER = 'artistEditor/setArtistEditorFilter';
export const SAVE_ARTIST_EDITOR = 'artistEditor/saveArtistEditor'; export const SAVE_ARTIST_EDITOR = 'artistEditor/saveArtistEditor';
export const BULK_DELETE_ARTIST = 'artistEditor/bulkDeleteArtist'; 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 // Action Creators
@ -107,8 +102,6 @@ export const setArtistEditorSort = createAction(SET_ARTIST_EDITOR_SORT);
export const setArtistEditorFilter = createAction(SET_ARTIST_EDITOR_FILTER); export const setArtistEditorFilter = createAction(SET_ARTIST_EDITOR_FILTER);
export const saveArtistEditor = createThunk(SAVE_ARTIST_EDITOR); export const saveArtistEditor = createThunk(SAVE_ARTIST_EDITOR);
export const bulkDeleteArtist = createThunk(BULK_DELETE_ARTIST); 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 // Action Handlers
@ -193,11 +186,6 @@ export const actionHandlers = handleThunks({
export const reducers = createHandleActions({ export const reducers = createHandleActions({
[SET_ARTIST_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section), [SET_ARTIST_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section),
[SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section), [SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section)
...createCustomFilterReducers(section, {
[customFilterHandlers.REMOVE]: REMOVE_ARTIST_EDITOR_CUSTOM_FILTER,
[customFilterHandlers.SAVE]: SAVE_ARTIST_EDITOR_CUSTOM_FILTER
})
}, defaultState, section); }, defaultState, section);

@ -1,12 +1,10 @@
import moment from 'moment'; import moment from 'moment';
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import customFilterHandlers from 'Utilities/customFilterHandlers';
import sortByName from 'Utilities/Array/sortByName'; import sortByName from 'Utilities/Array/sortByName';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props'; import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer'; import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import createCustomFilterReducers from './Creators/Reducers/createCustomFilterReducers';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
import { filters, filterPredicates } from './artistActions'; import { filters, filterPredicates } from './artistActions';
@ -292,7 +290,8 @@ export const defaultState = {
{ {
name: 'sizeOnDisk', name: 'sizeOnDisk',
label: 'Size on Disk', label: 'Size on Disk',
type: filterBuilderTypes.NUMBER type: filterBuilderTypes.NUMBER,
valueType: filterBuilderValueTypes.BYTES
}, },
{ {
name: 'genres', name: 'genres',
@ -324,8 +323,7 @@ export const defaultState = {
type: filterBuilderTypes.ARRAY, type: filterBuilderTypes.ARRAY,
valueType: filterBuilderValueTypes.TAG valueType: filterBuilderValueTypes.TAG
} }
], ]
customFilters: []
}; };
export const persistState = [ 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_POSTER_OPTION = 'artistIndex/setArtistPosterOption';
export const SET_ARTIST_BANNER_OPTION = 'artistIndex/setArtistBannerOption'; export const SET_ARTIST_BANNER_OPTION = 'artistIndex/setArtistBannerOption';
export const SET_ARTIST_OVERVIEW_OPTION = 'artistIndex/setArtistOverviewOption'; 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 // Action Creators
@ -363,8 +359,7 @@ export const setArtistTableOption = createAction(SET_ARTIST_TABLE_OPTION);
export const setArtistPosterOption = createAction(SET_ARTIST_POSTER_OPTION); export const setArtistPosterOption = createAction(SET_ARTIST_POSTER_OPTION);
export const setArtistBannerOption = createAction(SET_ARTIST_BANNER_OPTION); export const setArtistBannerOption = createAction(SET_ARTIST_BANNER_OPTION);
export const setArtistOverviewOption = createAction(SET_ARTIST_OVERVIEW_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 // Reducers
@ -413,11 +408,6 @@ export const reducers = createHandleActions({
...payload ...payload
} }
}; };
}, }
...createCustomFilterReducers(section, {
[customFilterHandlers.REMOVE]: REMOVE_ARTIST_CUSTOM_FILTER,
[customFilterHandlers.SAVE]: SAVE_ARTIST_CUSTOM_FILTER
})
}, defaultState, section); }, 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 app from './appActions';
import * as blacklist from './blacklistActions'; import * as blacklist from './blacklistActions';
import * as captcha from './captchaActions'; import * as captcha from './captchaActions';
import * as customFilters from './customFilterActions';
import * as devices from './deviceActions'; import * as devices from './deviceActions';
import * as calendar from './calendarActions'; import * as calendar from './calendarActions';
import * as commands from './commandActions'; import * as commands from './commandActions';
@ -35,6 +36,7 @@ export default [
captcha, captcha,
calendar, calendar,
commands, commands,
customFilters,
devices, devices,
albums, albums,
trackFiles, trackFiles,

@ -10,6 +10,7 @@ import createHandleActions from './Creators/createHandleActions';
// Variables // Variables
export const section = 'oAuth'; export const section = 'oAuth';
const callbackUrl = `${window.location.origin}${window.Lidarr.urlBase}/oauth.html`;
// //
// State // State
@ -64,6 +65,19 @@ function showOAuthWindow(url) {
return deferred.promise(); return deferred.promise();
} }
function executeIntermediateRequest(payload, ajaxOptions) {
return $.ajax(ajaxOptions).then((data) => {
return requestAction({
action: 'continueOAuth',
queryParams: {
...data,
callbackUrl
},
...payload
});
});
}
// //
// Action Handlers // Action Handlers
@ -72,7 +86,7 @@ export const actionHandlers = handleThunks({
[START_OAUTH]: function(getState, payload, dispatch) { [START_OAUTH]: function(getState, payload, dispatch) {
const actionPayload = { const actionPayload = {
action: 'startOAuth', action: 'startOAuth',
queryParams: { callbackUrl: `${window.location.origin}${window.Lidarr.urlBase}/oauth.html` }, queryParams: { callbackUrl },
...payload ...payload
}; };
@ -85,7 +99,16 @@ export const actionHandlers = handleThunks({
const promise = requestAction(actionPayload) const promise = requestAction(actionPayload)
.then((response) => { .then((response) => {
startResponse = response; startResponse = response;
return showOAuthWindow(response.oauthUrl);
if (response.oauthUrl) {
return showOAuthWindow(response.oauthUrl);
}
return executeIntermediateRequest(payload, response).then((intermediateResponse) => {
startResponse = intermediateResponse;
return showOAuthWindow(intermediateResponse.oauthUrl);
});
}) })
.then((queryParams) => { .then((queryParams) => {
return requestAction({ return requestAction({

@ -1,11 +1,9 @@
import $ from 'jquery'; import $ from 'jquery';
import { createAction } from 'redux-actions'; import { createAction } from 'redux-actions';
import customFilterHandlers from 'Utilities/customFilterHandlers';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props'; import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks'; import { createThunk, handleThunks } from 'Store/thunks';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer'; import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer'; import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import createCustomFilterReducers from './Creators/Reducers/createCustomFilterReducers';
import createFetchHandler from './Creators/createFetchHandler'; import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions'; import createHandleActions from './Creators/createHandleActions';
@ -45,8 +43,6 @@ export const defaultState = {
} }
}, },
selectedFilterKey: 'all',
filters: [ filters: [
{ {
key: 'all', key: 'all',
@ -143,9 +139,7 @@ export const defaultState = {
label: 'Rejections', label: 'Rejections',
type: filterBuilderTypes.NUMBER type: filterBuilderTypes.NUMBER
} }
], ]
customFilters: []
}; };
export const persistState = [ export const persistState = [
@ -163,9 +157,6 @@ export const CLEAR_RELEASES = 'releases/clearReleases';
export const GRAB_RELEASE = 'releases/grabRelease'; export const GRAB_RELEASE = 'releases/grabRelease';
export const UPDATE_RELEASE = 'releases/updateRelease'; export const UPDATE_RELEASE = 'releases/updateRelease';
export const SET_RELEASES_FILTER = 'releases/setReleasesFilter'; 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 // Action Creators
@ -177,9 +168,6 @@ export const clearReleases = createAction(CLEAR_RELEASES);
export const grabRelease = createThunk(GRAB_RELEASE); export const grabRelease = createThunk(GRAB_RELEASE);
export const updateRelease = createAction(UPDATE_RELEASE); export const updateRelease = createAction(UPDATE_RELEASE);
export const setReleasesFilter = createAction(SET_RELEASES_FILTER); 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 // Helpers
@ -266,11 +254,6 @@ export const reducers = createHandleActions({
}, },
[SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(section), [SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(section),
[SET_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(section), [SET_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(section)
...createCustomFilterReducers(section, {
[customFilterHandlers.REMOVE]: REMOVE_RELEASES_CUSTOM_FILTER,
[customFilterHandlers.SAVE]: SAVE_RELEASES_CUSTOM_FILTER
})
}, defaultState, 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 { applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { routerMiddleware } from 'react-router-redux'; import { routerMiddleware } from 'react-router-redux';
import sentryMiddleware from './sentryMiddleware'; import createSentryMiddleware from './createSentryMiddleware';
import persistState from './persistState'; import persistState from './persistState';
export default function(history) { export default function(history) {
const middlewares = []; const middlewares = [];
const ravenMiddleware = sentryMiddleware(); const sentryMiddleware = createSentryMiddleware();
if (ravenMiddleware) { if (sentryMiddleware) {
middlewares.push(ravenMiddleware); middlewares.push(sentryMiddleware);
} }
middlewares.push(routerMiddleware(history)); 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); 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) { function createClientSideCollectionSelector(section, uiSection) {
return createSelector( return createSelector(
(state) => _.get(state, section), (state) => _.get(state, section),
(state) => _.get(state, uiSection), (state) => _.get(state, uiSection),
(sectionState, uiSectionState = {}) => { createCustomFiltersSelector(section, uiSection),
const state = Object.assign({}, sectionState, uiSectionState); (sectionState, uiSectionState = {}, customFilters) => {
const state = Object.assign({}, sectionState, uiSectionState, { customFilters });
const filtered = filter(state.items, state); const filtered = filter(state.items, state);
const sorted = sort(filtered, state); const sorted = sort(filtered, state);
@ -107,6 +119,7 @@ function createClientSideCollectionSelector(section, uiSection) {
return { return {
...sectionState, ...sectionState,
...uiSectionState, ...uiSectionState,
customFilters,
items: sorted, items: sorted,
totalItems: state.items.length totalItems: state.items.length
}; };

@ -4,6 +4,7 @@ import titleCase from 'Utilities/String/titleCase';
import FieldSet from 'Components/FieldSet'; import FieldSet from 'Components/FieldSet';
import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import StartTime from './StartTime';
import styles from './About.css'; import styles from './About.css';
class About extends Component { class About extends Component {
@ -19,7 +20,10 @@ class About extends Component {
migrationVersion, migrationVersion,
appData, appData,
startupPath, startupPath,
mode mode,
startTime,
timeFormat,
longDateFormat
} = this.props; } = this.props;
return ( return (
@ -57,6 +61,17 @@ class About extends Component {
title="Mode" title="Mode"
data={titleCase(mode)} data={titleCase(mode)}
/> />
<DescriptionListItem
title="Uptime"
data={
<StartTime
startTime={startTime}
timeFormat={timeFormat}
longDateFormat={longDateFormat}
/>
}
/>
</DescriptionList> </DescriptionList>
</FieldSet> </FieldSet>
); );
@ -65,13 +80,16 @@ class About extends Component {
} }
About.propTypes = { About.propTypes = {
version: PropTypes.string, version: PropTypes.string.isRequired,
isMonoRuntime: PropTypes.bool, isMonoRuntime: PropTypes.bool.isRequired,
runtimeVersion: PropTypes.string, runtimeVersion: PropTypes.string.isRequired,
migrationVersion: PropTypes.number, migrationVersion: PropTypes.number.isRequired,
appData: PropTypes.string, appData: PropTypes.string.isRequired,
startupPath: PropTypes.string, startupPath: PropTypes.string.isRequired,
mode: PropTypes.string mode: PropTypes.string.isRequired,
startTime: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired
}; };
export default About; export default About;

@ -3,14 +3,18 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { fetchStatus } from 'Store/Actions/systemActions'; import { fetchStatus } from 'Store/Actions/systemActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import About from './About'; import About from './About';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.system.status, (state) => state.system.status,
(status) => { createUISettingsSelector(),
(status, uiSettings) => {
return { 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 = () => { getSelectedIds = () => {
const selectedIds = getSelectedIds(this.state.selectedState); const selectedIds = getSelectedIds(this.state.selectedState);
return _.uniq(_.map(selectedIds, (id) => { return selectedIds.reduce((acc, id) => {
return _.find(this.props.items, { id }).trackFileId; 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 TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import EpisodeQuality from 'Album/EpisodeQuality'; import TrackQuality from 'Album/TrackQuality';
import styles from './TrackFileEditorRow'; import styles from './TrackFileEditorRow';
function TrackFileEditorRow(props) { function TrackFileEditorRow(props) {
@ -42,7 +42,7 @@ function TrackFileEditorRow(props) {
</TableRowCell> </TableRowCell>
<TableRowCell> <TableRowCell>
<EpisodeQuality <TrackQuality
quality={quality} quality={quality}
/> />
</TableRowCell> </TableRowCell>

@ -3,7 +3,11 @@ export default function findSelectedFilters(selectedFilterKey, filters = [], cus
return []; 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) { if (!selectedFilter) {
// TODO: throw in dev // 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'; import getSectionState from 'Utilities/State/getSectionState';
function getProviderState(payload, getState, section) { function getProviderState(payload, getState, section) {
const id = payload.id; const {
id,
...otherPayload
} = payload;
const state = getSectionState(getState(), section, true); const state = getSectionState(getState(), section, true);
const pendingChanges = Object.assign({}, state.pendingChanges); const pendingChanges = Object.assign({}, state.pendingChanges, otherPayload);
const pendingFields = state.pendingChanges.fields || {}; const pendingFields = state.pendingChanges.fields || {};
delete 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 onSelectedChange
} = props; } = props;
if (!artist) {
return null;
}
return ( return (
<TableRow> <TableRow>
<TableSelectCell <TableSelectCell

@ -22,6 +22,7 @@
"@fortawesome/free-regular-svg-icons": "5.3.1", "@fortawesome/free-regular-svg-icons": "5.3.1",
"@fortawesome/free-solid-svg-icons": "5.3.1", "@fortawesome/free-solid-svg-icons": "5.3.1",
"@fortawesome/react-fontawesome": "0.1.3", "@fortawesome/react-fontawesome": "0.1.3",
"@sentry/browser": "4.0.4",
"autoprefixer": "9.1.5", "autoprefixer": "9.1.5",
"babel-core": "6.26.3", "babel-core": "6.26.3",
"babel-eslint": "9.0.0", "babel-eslint": "9.0.0",
@ -73,17 +74,15 @@
"postcss-simple-vars": "5.0.1", "postcss-simple-vars": "5.0.1",
"prop-types": "15.6.2", "prop-types": "15.6.2",
"qs": "6.5.2", "qs": "6.5.2",
"raven-for-redux": "1.3.1", "react": "16.5.2",
"raven-js": "3.27.0",
"react": "16.5.1",
"react-addons-shallow-compare": "15.6.2", "react-addons-shallow-compare": "15.6.2",
"react-async-script": "1.0.0", "react-async-script": "1.0.0",
"react-autosuggest": "9.4.1", "react-autosuggest": "9.4.2",
"react-custom-scrollbars": "4.2.1", "react-custom-scrollbars": "4.2.1",
"react-dnd": "5.0.0", "react-dnd": "5.0.0",
"react-dnd-html5-backend": "5.0.1", "react-dnd-html5-backend": "5.0.1",
"react-document-title": "2.0.3", "react-document-title": "2.0.3",
"react-dom": "16.5.1", "react-dom": "16.5.2",
"react-google-recaptcha": "1.0.2", "react-google-recaptcha": "1.0.2",
"react-lazyload": "2.3.0", "react-lazyload": "2.3.0",
"react-measure": "1.4.7", "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="Commands\CommandResource.cs" />
<Compile Include="Config\MetadataProviderConfigModule.cs" /> <Compile Include="Config\MetadataProviderConfigModule.cs" />
<Compile Include="Config\MetadataProviderConfigResource.cs" /> <Compile Include="Config\MetadataProviderConfigResource.cs" />
<Compile Include="CustomFilters\CustomFilterModule.cs" />
<Compile Include="CustomFilters\CustomFilterResource.cs" />
<Compile Include="ImportLists\ImportListModule.cs" /> <Compile Include="ImportLists\ImportListModule.cs" />
<Compile Include="ImportLists\ImportListResource.cs" /> <Compile Include="ImportLists\ImportListResource.cs" />
<Compile Include="Profiles\Metadata\MetadataProfileModule.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> <HintPath>..\packages\DotNet4.SocksProxy.1.3.4.0\lib\net40\Org.Mentalis.dll</HintPath>
<Private>True</Private> <Private>True</Private>
</Reference> </Reference>
<Reference Include="SharpRaven, Version=2.2.0.0, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="SharpRaven, Version=2.4.0.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\SharpRaven.2.2.0\lib\net45\SharpRaven.dll</HintPath> <HintPath>..\packages\SharpRaven.2.4.0\lib\net45\SharpRaven.dll</HintPath>
</Reference> </Reference>
<Reference Include="SocksWebProxy, Version=1.3.4.0, Culture=neutral, processorArchitecture=MSIL"> <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> <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.Configuration.Install" />
<Reference Include="System.Core" /> <Reference Include="System.Core" />
<Reference Include="System.Data" /> <Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.ServiceProcess" /> <Reference Include="System.ServiceProcess" />
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
<Reference Include="System.Xml" /> <Reference Include="System.Xml" />

@ -4,5 +4,5 @@
<package id="ICSharpCode.SharpZipLib.Patched" version="0.86.5" targetFramework="net461" /> <package id="ICSharpCode.SharpZipLib.Patched" version="0.86.5" targetFramework="net461" />
<package id="Newtonsoft.Json" version="11.0.2" targetFramework="net461" /> <package id="Newtonsoft.Json" version="11.0.2" targetFramework="net461" />
<package id="NLog" version="4.5.4" 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> </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…
Cancel
Save