mirror of https://github.com/lidarr/Lidarr
Remove Album Studio
(cherry picked from commit 6fce6c2bbcb1395a48f89d41d209b0ebe96042ee)
This commit is contained in:
parent
093d6c3c26
commit
f2b513c081
|
@ -1,36 +0,0 @@
|
||||||
.pageContentBodyWrapper {
|
|
||||||
display: flex;
|
|
||||||
flex: 1 0 1px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentBody {
|
|
||||||
composes: contentBody from '~Components/Page/PageContentBody.css';
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tableInnerContentBody {
|
|
||||||
composes: innerContentBody from '~Components/Page/PageContentBody.css';
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentBodyContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
|
||||||
.pageContentBodyWrapper {
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.contentBody {
|
|
||||||
flex-basis: 1px;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'contentBody': string;
|
|
||||||
'contentBodyContainer': string;
|
|
||||||
'pageContentBodyWrapper': string;
|
|
||||||
'tableInnerContentBody': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
|
@ -1,440 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { CellMeasurer, CellMeasurerCache } from 'react-virtualized';
|
|
||||||
import NoArtist from 'Artist/NoArtist';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import PageJumpBar from 'Components/Page/PageJumpBar';
|
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
import VirtualTable from 'Components/Table/VirtualTable';
|
|
||||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
|
||||||
import { align, sortDirections } from 'Helpers/Props';
|
|
||||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
|
||||||
import AlbumStudioFilterModalConnector from './AlbumStudioFilterModalConnector';
|
|
||||||
import AlbumStudioFooter from './AlbumStudioFooter';
|
|
||||||
import AlbumStudioRowConnector from './AlbumStudioRowConnector';
|
|
||||||
import AlbumStudioTableHeader from './AlbumStudioTableHeader';
|
|
||||||
import styles from './AlbumStudio.css';
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'status',
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'sortName',
|
|
||||||
label: () => translate('Name'),
|
|
||||||
isSortable: true,
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'albumCount',
|
|
||||||
label: () => translate('Albums'),
|
|
||||||
isSortable: false,
|
|
||||||
isVisible: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
class AlbumStudio extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.scrollerRef = React.createRef();
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
estimatedRowSize: 100,
|
|
||||||
jumpBarItems: { order: [] },
|
|
||||||
scrollIndex: null,
|
|
||||||
jumpCount: 0,
|
|
||||||
allSelected: false,
|
|
||||||
allUnselected: false,
|
|
||||||
lastToggled: null,
|
|
||||||
selectedState: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.cache = new CellMeasurerCache({
|
|
||||||
defaultHeight: 100,
|
|
||||||
fixedWidth: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.setSelectedState();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
isSaving,
|
|
||||||
saveError
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
scrollIndex,
|
|
||||||
jumpCount
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
|
||||||
this.onSelectAllChange({ value: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
// nasty hack to fix react-virtualized jumping incorrectly
|
|
||||||
// due to variable row heights
|
|
||||||
if (scrollIndex != null && scrollIndex > 0) {
|
|
||||||
if (jumpCount === 0) {
|
|
||||||
this.setState({
|
|
||||||
scrollIndex: scrollIndex - 1,
|
|
||||||
jumpCount: 1
|
|
||||||
});
|
|
||||||
} else if (jumpCount === 1) {
|
|
||||||
this.setState({
|
|
||||||
scrollIndex: scrollIndex + 1,
|
|
||||||
jumpCount: 2
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
scrollIndex: null,
|
|
||||||
jumpCount: 0
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setJumpBarItems() {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
sortKey,
|
|
||||||
sortDirection
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
// Reset if not sorting by sortName
|
|
||||||
if (sortKey !== 'sortName') {
|
|
||||||
this.setState({ jumpBarItems: { order: [] } });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const characters = _.reduce(items, (acc, item) => {
|
|
||||||
let char = item.sortName.charAt(0);
|
|
||||||
|
|
||||||
if (!isNaN(char)) {
|
|
||||||
char = '#';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (char in acc) {
|
|
||||||
acc[char] = acc[char] + 1;
|
|
||||||
} else {
|
|
||||||
acc[char] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const order = Object.keys(characters).sort();
|
|
||||||
|
|
||||||
// Reverse if sorting descending
|
|
||||||
if (sortDirection === sortDirections.DESCENDING) {
|
|
||||||
order.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
const jumpBarItems = {
|
|
||||||
characters,
|
|
||||||
order
|
|
||||||
};
|
|
||||||
|
|
||||||
this.setState({ jumpBarItems });
|
|
||||||
}
|
|
||||||
|
|
||||||
getSelectedIds = () => {
|
|
||||||
if (this.state.allUnselected) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return getSelectedIds(this.state.selectedState);
|
|
||||||
};
|
|
||||||
|
|
||||||
setSelectedState = () => {
|
|
||||||
const {
|
|
||||||
items
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
selectedState
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const newSelectedState = {};
|
|
||||||
|
|
||||||
items.forEach((artist) => {
|
|
||||||
const isItemSelected = selectedState[artist.id];
|
|
||||||
|
|
||||||
if (isItemSelected) {
|
|
||||||
newSelectedState[artist.id] = isItemSelected;
|
|
||||||
} else {
|
|
||||||
newSelectedState[artist.id] = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectedCount = getSelectedIds(newSelectedState).length;
|
|
||||||
const newStateCount = Object.keys(newSelectedState).length;
|
|
||||||
let isAllSelected = false;
|
|
||||||
let isAllUnselected = false;
|
|
||||||
|
|
||||||
if (selectedCount === 0) {
|
|
||||||
isAllUnselected = true;
|
|
||||||
} else if (selectedCount === newStateCount) {
|
|
||||||
isAllSelected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
|
||||||
};
|
|
||||||
|
|
||||||
estimateRowHeight = (width) => {
|
|
||||||
const {
|
|
||||||
albumCount,
|
|
||||||
items
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (albumCount === undefined || albumCount === 0 || items.length === 0) {
|
|
||||||
return 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
// guess 250px per album entry
|
|
||||||
// available width is total width less 186px for select, status etc
|
|
||||||
const cols = Math.max(Math.floor((width - 186) / 250), 1);
|
|
||||||
const albumsPerArtist = albumCount / items.length;
|
|
||||||
const albumRowsPerArtist = albumsPerArtist / cols;
|
|
||||||
|
|
||||||
// each row is 23px per album row plus 16px padding
|
|
||||||
return albumRowsPerArtist * 23 + 16;
|
|
||||||
};
|
|
||||||
|
|
||||||
rowRenderer = ({ key, rowIndex, parent, style }) => {
|
|
||||||
const {
|
|
||||||
items
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
selectedState
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const item = items[rowIndex];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CellMeasurer
|
|
||||||
key={key}
|
|
||||||
cache={this.cache}
|
|
||||||
parent={parent}
|
|
||||||
columnIndex={0}
|
|
||||||
rowIndex={rowIndex}
|
|
||||||
>
|
|
||||||
{({ registerChild }) => (
|
|
||||||
<VirtualTableRow
|
|
||||||
ref={registerChild}
|
|
||||||
style={style}
|
|
||||||
>
|
|
||||||
<AlbumStudioRowConnector
|
|
||||||
key={item.id}
|
|
||||||
artistId={item.id}
|
|
||||||
isSelected={selectedState[item.id]}
|
|
||||||
onSelectedChange={this.onSelectedChange}
|
|
||||||
/>
|
|
||||||
</VirtualTableRow>
|
|
||||||
)}
|
|
||||||
</CellMeasurer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onSelectAllChange = ({ value }) => {
|
|
||||||
this.setState(selectAll(this.state.selectedState, value));
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
|
||||||
this.setState((state) => {
|
|
||||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onSelectAllPress = () => {
|
|
||||||
this.onSelectAllChange({ value: !this.state.allSelected });
|
|
||||||
};
|
|
||||||
|
|
||||||
onUpdateSelectedPress = (changes) => {
|
|
||||||
this.props.onUpdateSelectedPress({
|
|
||||||
artistIds: this.getSelectedIds(),
|
|
||||||
...changes
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onJumpBarItemPress = (jumpToCharacter) => {
|
|
||||||
const scrollIndex = getIndexOfFirstCharacter(this.props.items, jumpToCharacter);
|
|
||||||
|
|
||||||
if (scrollIndex != null) {
|
|
||||||
this.setState({ scrollIndex });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onGridRecompute = (width) => {
|
|
||||||
this.setJumpBarItems();
|
|
||||||
this.setSelectedState();
|
|
||||||
this.setState({ estimatedRowSize: this.estimateRowHeight(width) });
|
|
||||||
this.cache.clearAll();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
totalItems,
|
|
||||||
items,
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
customFilters,
|
|
||||||
sortKey,
|
|
||||||
sortDirection,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
isSmallScreen,
|
|
||||||
onSortPress,
|
|
||||||
onFilterSelect
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
allSelected,
|
|
||||||
allUnselected,
|
|
||||||
estimatedRowSize,
|
|
||||||
jumpBarItems,
|
|
||||||
scrollIndex
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('AlbumStudio')}>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection />
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
|
||||||
<FilterMenu
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
|
||||||
filters={filters}
|
|
||||||
customFilters={customFilters}
|
|
||||||
filterModalConnectorComponent={AlbumStudioFilterModalConnector}
|
|
||||||
onFilterSelect={onFilterSelect}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<div className={styles.pageContentBodyWrapper}>
|
|
||||||
<PageContentBody
|
|
||||||
ref={this.scrollerRef}
|
|
||||||
className={styles.contentBody}
|
|
||||||
innerClassName={styles.innerContentBody}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isFetching && !isPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<div>{getErrorMessage(error, 'Failed to load artist from API')}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!error &&
|
|
||||||
isPopulated &&
|
|
||||||
!!items.length &&
|
|
||||||
this.scrollerRef.current ?
|
|
||||||
<div className={styles.contentBodyContainer}>
|
|
||||||
<VirtualTable
|
|
||||||
items={items}
|
|
||||||
scrollIndex={scrollIndex}
|
|
||||||
columns={columns}
|
|
||||||
scroller={this.scrollerRef.current}
|
|
||||||
isSmallScreen={isSmallScreen}
|
|
||||||
overscanRowCount={5}
|
|
||||||
rowRenderer={this.rowRenderer}
|
|
||||||
header={
|
|
||||||
<AlbumStudioTableHeader
|
|
||||||
columns={columns}
|
|
||||||
sortKey={sortKey}
|
|
||||||
sortDirection={sortDirection}
|
|
||||||
onSortPress={onSortPress}
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
onSelectAllChange={this.onSelectAllChange}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
sortKey={sortKey}
|
|
||||||
sortDirection={sortDirection}
|
|
||||||
deferredMeasurementCache={this.cache}
|
|
||||||
rowHeight={this.cache.rowHeight}
|
|
||||||
estimatedRowSize={estimatedRowSize}
|
|
||||||
onRecompute={this.onGridRecompute}
|
|
||||||
/>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!error && isPopulated && !items.length &&
|
|
||||||
<NoArtist totalItems={totalItems} />
|
|
||||||
}
|
|
||||||
</PageContentBody>
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !!jumpBarItems.order.length &&
|
|
||||||
<PageJumpBar
|
|
||||||
items={jumpBarItems}
|
|
||||||
onItemPress={this.onJumpBarItemPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AlbumStudioFooter
|
|
||||||
selectedCount={this.getSelectedIds().length}
|
|
||||||
isSaving={isSaving}
|
|
||||||
saveError={saveError}
|
|
||||||
onUpdateSelectedPress={this.onUpdateSelectedPress}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AlbumStudio.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
totalItems: PropTypes.number.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
albumCount: PropTypes.number.isRequired,
|
|
||||||
sortKey: PropTypes.string,
|
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
|
||||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
onSortPress: PropTypes.func.isRequired,
|
|
||||||
onFilterSelect: PropTypes.func.isRequired,
|
|
||||||
onUpdateSelectedPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AlbumStudio;
|
|
|
@ -1,39 +0,0 @@
|
||||||
.album {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
overflow: hidden;
|
|
||||||
margin: 2px 4px;
|
|
||||||
border: 1px solid var(--borderColor);
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: var(--albumBackgroundColor);
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
padding: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.albumType {
|
|
||||||
padding: 0 4px;
|
|
||||||
border-width: 0 1px;
|
|
||||||
border-style: solid;
|
|
||||||
border-color: var(--borderColor);
|
|
||||||
background-color: var(--albumBackgroundColor);
|
|
||||||
color: var(--defaultColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.tracks {
|
|
||||||
padding: 0 4px;
|
|
||||||
background-color: var(--trackBackgroundColor);
|
|
||||||
color: var(--defaultColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.allTracks {
|
|
||||||
background-color: color(#27c24c saturation(-25%));
|
|
||||||
color: var(--white);
|
|
||||||
}
|
|
||||||
|
|
||||||
.missingWanted {
|
|
||||||
background-color: color(#f05050 saturation(-20%));
|
|
||||||
color: var(--white);
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'album': string;
|
|
||||||
'albumType': string;
|
|
||||||
'allTracks': string;
|
|
||||||
'info': string;
|
|
||||||
'missingWanted': string;
|
|
||||||
'tracks': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
|
@ -1,102 +0,0 @@
|
||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './AlbumStudioAlbum.css';
|
|
||||||
|
|
||||||
class AlbumStudioAlbum extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onAlbumMonitoredPress = () => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
monitored
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.onAlbumMonitoredPress(id, !monitored);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
disambiguation,
|
|
||||||
albumType,
|
|
||||||
monitored,
|
|
||||||
statistics,
|
|
||||||
isSaving
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
trackFileCount,
|
|
||||||
totalTrackCount,
|
|
||||||
percentOfTracks
|
|
||||||
} = statistics;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.album}>
|
|
||||||
<div className={styles.info}>
|
|
||||||
<MonitorToggleButton
|
|
||||||
monitored={monitored}
|
|
||||||
isSaving={isSaving}
|
|
||||||
onPress={this.onAlbumMonitoredPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
{
|
|
||||||
disambiguation ? `${title} (${disambiguation})` : `${title}`
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.albumType}>
|
|
||||||
<span>
|
|
||||||
{
|
|
||||||
`${albumType}`
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.tracks,
|
|
||||||
percentOfTracks < 100 && monitored && styles.missingWanted,
|
|
||||||
percentOfTracks === 100 && styles.allTracks
|
|
||||||
)}
|
|
||||||
title={translate('TrackFileCounttotalTrackCountTracksDownloadedInterp', [trackFileCount, totalTrackCount])}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
totalTrackCount === 0 ? '0/0' : `${trackFileCount}/${totalTrackCount}`
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AlbumStudioAlbum.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
disambiguation: PropTypes.string,
|
|
||||||
albumType: PropTypes.string.isRequired,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
statistics: PropTypes.object.isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
onAlbumMonitoredPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
AlbumStudioAlbum.defaultProps = {
|
|
||||||
isSaving: false,
|
|
||||||
statistics: {
|
|
||||||
trackFileCount: 0,
|
|
||||||
totalTrackCount: 0,
|
|
||||||
percentOfTracks: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AlbumStudioAlbum;
|
|
|
@ -1,116 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { clearAlbums, fetchAlbums } from 'Store/Actions/albumActions';
|
|
||||||
import { saveAlbumStudio, setAlbumStudioFilter, setAlbumStudioSort } from 'Store/Actions/albumStudioActions';
|
|
||||||
import createArtistClientSideCollectionItemsSelector from 'Store/Selectors/createArtistClientSideCollectionItemsSelector';
|
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import AlbumStudio from './AlbumStudio';
|
|
||||||
|
|
||||||
function createAlbumFetchStateSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.albums.items.length,
|
|
||||||
(state) => state.albums.isFetching,
|
|
||||||
(state) => state.albums.isPopulated,
|
|
||||||
(length, isFetching, isPopulated) => {
|
|
||||||
const albumCount = (!isFetching && isPopulated) ? length : 0;
|
|
||||||
return {
|
|
||||||
albumCount,
|
|
||||||
isFetching,
|
|
||||||
isPopulated
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createAlbumFetchStateSelector(),
|
|
||||||
createArtistClientSideCollectionItemsSelector('albumStudio'),
|
|
||||||
createDimensionsSelector(),
|
|
||||||
(albums, artist, dimensionsState) => {
|
|
||||||
const isPopulated = albums.isPopulated && artist.isPopulated;
|
|
||||||
const isFetching = artist.isFetching || albums.isFetching;
|
|
||||||
return {
|
|
||||||
...artist,
|
|
||||||
isPopulated,
|
|
||||||
isFetching,
|
|
||||||
albumCount: albums.albumCount,
|
|
||||||
isSmallScreen: dimensionsState.isSmallScreen
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchAlbums,
|
|
||||||
clearAlbums,
|
|
||||||
setAlbumStudioSort,
|
|
||||||
setAlbumStudioFilter,
|
|
||||||
saveAlbumStudio
|
|
||||||
};
|
|
||||||
|
|
||||||
class AlbumStudioConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.populate();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.unpopulate();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
populate = () => {
|
|
||||||
this.props.fetchAlbums();
|
|
||||||
};
|
|
||||||
|
|
||||||
unpopulate = () => {
|
|
||||||
this.props.clearAlbums();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onSortPress = (sortKey) => {
|
|
||||||
this.props.setAlbumStudioSort({ sortKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onFilterSelect = (selectedFilterKey) => {
|
|
||||||
this.props.setAlbumStudioFilter({ selectedFilterKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onUpdateSelectedPress = (payload) => {
|
|
||||||
this.props.saveAlbumStudio(payload);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<AlbumStudio
|
|
||||||
{...this.props}
|
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onFilterSelect={this.onFilterSelect}
|
|
||||||
onUpdateSelectedPress={this.onUpdateSelectedPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AlbumStudioConnector.propTypes = {
|
|
||||||
setAlbumStudioSort: PropTypes.func.isRequired,
|
|
||||||
setAlbumStudioFilter: PropTypes.func.isRequired,
|
|
||||||
fetchAlbums: PropTypes.func.isRequired,
|
|
||||||
clearAlbums: PropTypes.func.isRequired,
|
|
||||||
saveAlbumStudio: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioConnector);
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import FilterModal from 'Components/Filter/FilterModal';
|
|
||||||
import { setAlbumStudioFilter } from 'Store/Actions/albumStudioActions';
|
|
||||||
|
|
||||||
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);
|
|
|
@ -1,14 +0,0 @@
|
||||||
.inputContainer {
|
|
||||||
margin-right: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
margin-bottom: 3px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.updateSelectedButton {
|
|
||||||
composes: button from '~Components/Link/SpinnerButton.css';
|
|
||||||
|
|
||||||
height: 35px;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'inputContainer': string;
|
|
||||||
'label': string;
|
|
||||||
'updateSelectedButton': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
|
@ -1,174 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import MonitorAlbumsSelectInput from 'Components/Form/MonitorAlbumsSelectInput';
|
|
||||||
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './AlbumStudioFooter.css';
|
|
||||||
|
|
||||||
const NO_CHANGE = 'noChange';
|
|
||||||
|
|
||||||
class AlbumStudioFooter extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
monitored: NO_CHANGE,
|
|
||||||
monitor: NO_CHANGE,
|
|
||||||
monitorNewItems: NO_CHANGE
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
isSaving,
|
|
||||||
saveError
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
|
||||||
this.setState({
|
|
||||||
monitored: NO_CHANGE,
|
|
||||||
monitor: NO_CHANGE,
|
|
||||||
monitorNewItems: NO_CHANGE
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.setState({ [name]: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onUpdateSelectedPress = () => {
|
|
||||||
const {
|
|
||||||
monitor,
|
|
||||||
monitored,
|
|
||||||
monitorNewItems
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const changes = {};
|
|
||||||
|
|
||||||
if (monitored !== NO_CHANGE) {
|
|
||||||
changes.monitored = monitored === 'monitored';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (monitor !== NO_CHANGE) {
|
|
||||||
changes.monitor = monitor;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (monitorNewItems !== NO_CHANGE) {
|
|
||||||
changes.monitorNewItems = monitorNewItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onUpdateSelectedPress(changes);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
selectedCount,
|
|
||||||
isSaving
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
monitored,
|
|
||||||
monitor,
|
|
||||||
monitorNewItems
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const monitoredOptions = [
|
|
||||||
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
|
|
||||||
{ key: 'monitored', value: translate('Monitored') },
|
|
||||||
{ key: 'unmonitored', value: translate('Unmonitored') }
|
|
||||||
];
|
|
||||||
|
|
||||||
const noChanges = monitored === NO_CHANGE &&
|
|
||||||
monitor === NO_CHANGE &&
|
|
||||||
monitorNewItems === NO_CHANGE;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContentFooter>
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<div className={styles.label}>
|
|
||||||
{translate('MonitorArtist')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.SELECT}
|
|
||||||
name="monitored"
|
|
||||||
value={monitored}
|
|
||||||
values={monitoredOptions}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<div className={styles.label}>
|
|
||||||
{translate('MonitorExistingAlbums')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MonitorAlbumsSelectInput
|
|
||||||
name="monitor"
|
|
||||||
value={monitor}
|
|
||||||
includeNoChange={true}
|
|
||||||
includeNoChangeDisabled={false}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.inputContainer}>
|
|
||||||
<div className={styles.label}>
|
|
||||||
{translate('MonitorNewAlbums')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<MonitorNewItemsSelectInput
|
|
||||||
name="monitorNewItems"
|
|
||||||
value={monitorNewItems}
|
|
||||||
includeNoChange={true}
|
|
||||||
includeNoChangeDisabled={false}
|
|
||||||
isDisabled={!selectedCount}
|
|
||||||
onChange={this.onInputChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className={styles.label}>
|
|
||||||
{translate('CountArtistsSelected', { count: selectedCount })}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SpinnerButton
|
|
||||||
className={styles.updateSelectedButton}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
isSpinning={isSaving}
|
|
||||||
isDisabled={!selectedCount || noChanges}
|
|
||||||
onPress={this.onUpdateSelectedPress}
|
|
||||||
>
|
|
||||||
{translate('UpdateSelected')}
|
|
||||||
</SpinnerButton>
|
|
||||||
</div>
|
|
||||||
</PageContentFooter>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AlbumStudioFooter.propTypes = {
|
|
||||||
selectedCount: PropTypes.number.isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
onUpdateSelectedPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AlbumStudioFooter;
|
|
|
@ -1,41 +0,0 @@
|
||||||
.cell {
|
|
||||||
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selectCell {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 0;
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
flex-shrink: 0;
|
|
||||||
min-width: 110px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.albums {
|
|
||||||
composes: cell;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 4;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
min-width: 400px;
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'albums': string;
|
|
||||||
'cell': string;
|
|
||||||
'selectCell': string;
|
|
||||||
'status': string;
|
|
||||||
'title': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
|
@ -1,95 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
|
||||||
import ArtistStatusCell from 'Artist/Index/Table/ArtistStatusCell';
|
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
|
||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
|
||||||
import AlbumStudioAlbum from './AlbumStudioAlbum';
|
|
||||||
import styles from './AlbumStudioRow.css';
|
|
||||||
|
|
||||||
class AlbumStudioRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
artistId,
|
|
||||||
status,
|
|
||||||
foreignArtistId,
|
|
||||||
artistName,
|
|
||||||
artistType,
|
|
||||||
monitored,
|
|
||||||
albums,
|
|
||||||
isSaving,
|
|
||||||
isSelected,
|
|
||||||
onSelectedChange,
|
|
||||||
onArtistMonitoredPress,
|
|
||||||
onAlbumMonitoredPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<VirtualTableSelectCell
|
|
||||||
className={styles.selectCell}
|
|
||||||
id={artistId}
|
|
||||||
isSelected={isSelected}
|
|
||||||
onSelectedChange={onSelectedChange}
|
|
||||||
isDisabled={false}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ArtistStatusCell
|
|
||||||
className={styles.status}
|
|
||||||
artistType={artistType}
|
|
||||||
monitored={monitored}
|
|
||||||
status={status}
|
|
||||||
isSaving={isSaving}
|
|
||||||
onMonitoredPress={onArtistMonitoredPress}
|
|
||||||
component={VirtualTableRowCell}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.title}>
|
|
||||||
<ArtistNameLink
|
|
||||||
foreignArtistId={foreignArtistId}
|
|
||||||
artistName={artistName}
|
|
||||||
/>
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
|
|
||||||
<VirtualTableRowCell className={styles.albums}>
|
|
||||||
{
|
|
||||||
albums.map((album) => {
|
|
||||||
return (
|
|
||||||
<AlbumStudioAlbum
|
|
||||||
key={album.id}
|
|
||||||
{...album}
|
|
||||||
onAlbumMonitoredPress={onAlbumMonitoredPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</VirtualTableRowCell>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AlbumStudioRow.propTypes = {
|
|
||||||
artistId: PropTypes.number.isRequired,
|
|
||||||
status: PropTypes.string.isRequired,
|
|
||||||
foreignArtistId: PropTypes.string.isRequired,
|
|
||||||
artistName: PropTypes.string.isRequired,
|
|
||||||
artistType: PropTypes.string,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
isSelected: PropTypes.bool,
|
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
|
||||||
onArtistMonitoredPress: PropTypes.func.isRequired,
|
|
||||||
onAlbumMonitoredPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
AlbumStudioRow.defaultProps = {
|
|
||||||
isSaving: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AlbumStudioRow;
|
|
|
@ -1,94 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { toggleAlbumsMonitored } from 'Store/Actions/albumActions';
|
|
||||||
import { toggleArtistMonitored } from 'Store/Actions/artistActions';
|
|
||||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
|
||||||
import AlbumStudioRow from './AlbumStudioRow';
|
|
||||||
|
|
||||||
// Use a const to share the reselect cache between instances
|
|
||||||
const getAlbumMap = createSelector(
|
|
||||||
(state) => state.albums.items,
|
|
||||||
(albums) => {
|
|
||||||
return albums.reduce((acc, curr) => {
|
|
||||||
(acc[curr.artistId] = acc[curr.artistId] || []).push(curr);
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createArtistSelector(),
|
|
||||||
getAlbumMap,
|
|
||||||
(artist, albumMap) => {
|
|
||||||
const albumsInArtist = albumMap.hasOwnProperty(artist.id) ? albumMap[artist.id] : [];
|
|
||||||
const sortedAlbums = _.orderBy(albumsInArtist, 'releaseDate', 'desc');
|
|
||||||
|
|
||||||
return {
|
|
||||||
...artist,
|
|
||||||
artistId: artist.id,
|
|
||||||
artistName: artist.artistName,
|
|
||||||
monitored: artist.monitored,
|
|
||||||
status: artist.status,
|
|
||||||
isSaving: artist.isSaving,
|
|
||||||
albums: sortedAlbums
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
toggleArtistMonitored,
|
|
||||||
toggleAlbumsMonitored
|
|
||||||
};
|
|
||||||
|
|
||||||
class AlbumStudioRowConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onArtistMonitoredPress = () => {
|
|
||||||
const {
|
|
||||||
artistId,
|
|
||||||
monitored
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.toggleArtistMonitored({
|
|
||||||
artistId,
|
|
||||||
monitored: !monitored
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onAlbumMonitoredPress = (albumId, monitored) => {
|
|
||||||
const albumIds = [albumId];
|
|
||||||
this.props.toggleAlbumsMonitored({
|
|
||||||
albumIds,
|
|
||||||
monitored
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<AlbumStudioRow
|
|
||||||
{...this.props}
|
|
||||||
onArtistMonitoredPress={this.onArtistMonitoredPress}
|
|
||||||
onAlbumMonitoredPress={this.onAlbumMonitoredPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AlbumStudioRowConnector.propTypes = {
|
|
||||||
artistId: PropTypes.number.isRequired,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
toggleArtistMonitored: PropTypes.func.isRequired,
|
|
||||||
toggleAlbumsMonitored: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioRowConnector);
|
|
|
@ -1,18 +0,0 @@
|
||||||
.status {
|
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
|
||||||
|
|
||||||
flex: 0 0 60px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sortName {
|
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
|
||||||
|
|
||||||
flex: 0 0 110px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.albumCount {
|
|
||||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
|
||||||
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// This file is automatically generated.
|
|
||||||
// Please do not change this file!
|
|
||||||
interface CssExports {
|
|
||||||
'albumCount': string;
|
|
||||||
'sortName': string;
|
|
||||||
'status': string;
|
|
||||||
}
|
|
||||||
export const cssExports: CssExports;
|
|
||||||
export default cssExports;
|
|
|
@ -1,61 +0,0 @@
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
|
||||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
|
||||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
|
||||||
import styles from './AlbumStudioTableHeader.css';
|
|
||||||
|
|
||||||
function AlbumStudioTableHeader(props) {
|
|
||||||
const {
|
|
||||||
columns,
|
|
||||||
allSelected,
|
|
||||||
allUnselected,
|
|
||||||
onSelectAllChange,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTableHeader>
|
|
||||||
<VirtualTableSelectAllHeaderCell
|
|
||||||
allSelected={allSelected}
|
|
||||||
allUnselected={allUnselected}
|
|
||||||
onSelectAllChange={onSelectAllChange}
|
|
||||||
/>
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
isSortable,
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VirtualTableHeaderCell
|
|
||||||
key={name}
|
|
||||||
className={styles[name]}
|
|
||||||
name={name}
|
|
||||||
isSortable={isSortable}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{typeof label === 'function' ? label() : label}
|
|
||||||
</VirtualTableHeaderCell>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</VirtualTableHeader>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AlbumStudioTableHeader.propTypes = {
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
allSelected: PropTypes.bool.isRequired,
|
|
||||||
allUnselected: PropTypes.bool.isRequired,
|
|
||||||
onSelectAllChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AlbumStudioTableHeader;
|
|
|
@ -5,7 +5,6 @@ import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
|
||||||
import HistoryConnector from 'Activity/History/HistoryConnector';
|
import HistoryConnector from 'Activity/History/HistoryConnector';
|
||||||
import QueueConnector from 'Activity/Queue/QueueConnector';
|
import QueueConnector from 'Activity/Queue/QueueConnector';
|
||||||
import AlbumDetailsPageConnector from 'Album/Details/AlbumDetailsPageConnector';
|
import AlbumDetailsPageConnector from 'Album/Details/AlbumDetailsPageConnector';
|
||||||
import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector';
|
|
||||||
import ArtistDetailsPageConnector from 'Artist/Details/ArtistDetailsPageConnector';
|
import ArtistDetailsPageConnector from 'Artist/Details/ArtistDetailsPageConnector';
|
||||||
import ArtistIndex from 'Artist/Index/ArtistIndex';
|
import ArtistIndex from 'Artist/Index/ArtistIndex';
|
||||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||||
|
@ -90,7 +89,15 @@ function AppRoutes(props) {
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/albumstudio"
|
path="/albumstudio"
|
||||||
component={AlbumStudioConnector}
|
exact={true}
|
||||||
|
render={() => {
|
||||||
|
return (
|
||||||
|
<Redirect
|
||||||
|
to={getPathWithUrlBase('/')}
|
||||||
|
component={app}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
|
|
|
@ -29,10 +29,6 @@ const links = [
|
||||||
title: () => translate('AddNew'),
|
title: () => translate('AddNew'),
|
||||||
to: '/add/search'
|
to: '/add/search'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: () => translate('AlbumStudio'),
|
|
||||||
to: '/albumstudio'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: () => translate('UnmappedFiles'),
|
title: () => translate('UnmappedFiles'),
|
||||||
to: '/unmapped'
|
to: '/unmapped'
|
||||||
|
|
|
@ -1,167 +0,0 @@
|
||||||
import { createAction } from 'redux-actions';
|
|
||||||
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
|
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import { fetchAlbums } from './albumActions';
|
|
||||||
import { filterPredicates, filters } from './artistActions';
|
|
||||||
import { set } from './baseActions';
|
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
|
||||||
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
|
|
||||||
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Variables
|
|
||||||
|
|
||||||
export const section = 'albumStudio';
|
|
||||||
|
|
||||||
//
|
|
||||||
// State
|
|
||||||
|
|
||||||
export const defaultState = {
|
|
||||||
isSaving: false,
|
|
||||||
saveError: null,
|
|
||||||
sortKey: 'sortName',
|
|
||||||
sortDirection: sortDirections.ASCENDING,
|
|
||||||
secondarySortKey: 'sortName',
|
|
||||||
secondarySortDirection: sortDirections.ASCENDING,
|
|
||||||
selectedFilterKey: 'all',
|
|
||||||
filters,
|
|
||||||
filterPredicates,
|
|
||||||
|
|
||||||
filterBuilderProps: [
|
|
||||||
{
|
|
||||||
name: 'monitored',
|
|
||||||
label: () => translate('Monitored'),
|
|
||||||
type: filterBuilderTypes.EXACT,
|
|
||||||
valueType: filterBuilderValueTypes.BOOL
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'status',
|
|
||||||
label: () => translate('Status'),
|
|
||||||
type: filterBuilderTypes.EXACT,
|
|
||||||
valueType: filterBuilderValueTypes.ARTIST_STATUS
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'artistType',
|
|
||||||
label: () => translate('ArtistType'),
|
|
||||||
type: filterBuilderTypes.EXACT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'qualityProfileId',
|
|
||||||
label: () => translate('QualityProfile'),
|
|
||||||
type: filterBuilderTypes.EXACT,
|
|
||||||
valueType: filterBuilderValueTypes.QUALITY_PROFILE
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'metadataProfileId',
|
|
||||||
label: () => translate('MetadataProfile'),
|
|
||||||
type: filterBuilderTypes.EXACT,
|
|
||||||
valueType: filterBuilderValueTypes.METADATA_PROFILE
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'rootFolderPath',
|
|
||||||
label: () => translate('RootFolderPath'),
|
|
||||||
type: filterBuilderTypes.EXACT
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'tags',
|
|
||||||
label: () => translate('Tags'),
|
|
||||||
type: filterBuilderTypes.ARRAY,
|
|
||||||
valueType: filterBuilderValueTypes.TAG
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
export const persistState = [
|
|
||||||
'albumStudio.sortKey',
|
|
||||||
'albumStudio.sortDirection',
|
|
||||||
'albumStudio.selectedFilterKey',
|
|
||||||
'albumStudio.customFilters'
|
|
||||||
];
|
|
||||||
|
|
||||||
//
|
|
||||||
// Actions Types
|
|
||||||
|
|
||||||
export const SET_ALBUM_STUDIO_SORT = 'albumStudio/setAlbumStudioSort';
|
|
||||||
export const SET_ALBUM_STUDIO_FILTER = 'albumStudio/setAlbumStudioFilter';
|
|
||||||
export const SAVE_ALBUM_STUDIO = 'albumStudio/saveAlbumStudio';
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Creators
|
|
||||||
|
|
||||||
export const setAlbumStudioSort = createAction(SET_ALBUM_STUDIO_SORT);
|
|
||||||
export const setAlbumStudioFilter = createAction(SET_ALBUM_STUDIO_FILTER);
|
|
||||||
export const saveAlbumStudio = createThunk(SAVE_ALBUM_STUDIO);
|
|
||||||
|
|
||||||
//
|
|
||||||
// Action Handlers
|
|
||||||
|
|
||||||
export const actionHandlers = handleThunks({
|
|
||||||
|
|
||||||
[SAVE_ALBUM_STUDIO]: function(getState, payload, dispatch) {
|
|
||||||
const {
|
|
||||||
artistIds,
|
|
||||||
monitor,
|
|
||||||
monitored,
|
|
||||||
monitorNewItems
|
|
||||||
} = payload;
|
|
||||||
|
|
||||||
const artists = [];
|
|
||||||
|
|
||||||
artistIds.forEach((id) => {
|
|
||||||
const artistsToUpdate = { id };
|
|
||||||
|
|
||||||
if (payload.hasOwnProperty('monitored')) {
|
|
||||||
artistsToUpdate.monitored = monitored;
|
|
||||||
}
|
|
||||||
|
|
||||||
artists.push(artistsToUpdate);
|
|
||||||
});
|
|
||||||
|
|
||||||
dispatch(set({
|
|
||||||
section,
|
|
||||||
isSaving: true
|
|
||||||
}));
|
|
||||||
|
|
||||||
const promise = createAjaxRequest({
|
|
||||||
url: '/albumStudio',
|
|
||||||
method: 'POST',
|
|
||||||
data: JSON.stringify({
|
|
||||||
artist: artists,
|
|
||||||
monitoringOptions: { monitor },
|
|
||||||
monitorNewItems
|
|
||||||
}),
|
|
||||||
dataType: 'json'
|
|
||||||
}).request;
|
|
||||||
|
|
||||||
promise.done((data) => {
|
|
||||||
dispatch(fetchAlbums());
|
|
||||||
|
|
||||||
dispatch(set({
|
|
||||||
section,
|
|
||||||
isSaving: false,
|
|
||||||
saveError: null
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
promise.fail((xhr) => {
|
|
||||||
dispatch(set({
|
|
||||||
section,
|
|
||||||
isSaving: false,
|
|
||||||
saveError: xhr
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
//
|
|
||||||
// Reducers
|
|
||||||
|
|
||||||
export const reducers = createHandleActions({
|
|
||||||
|
|
||||||
[SET_ALBUM_STUDIO_SORT]: createSetClientSideCollectionSortReducer(section),
|
|
||||||
[SET_ALBUM_STUDIO_FILTER]: createSetClientSideCollectionFilterReducer(section)
|
|
||||||
|
|
||||||
}, defaultState, section);
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import * as albums from './albumActions';
|
import * as albums from './albumActions';
|
||||||
import * as albumHistory from './albumHistoryActions';
|
import * as albumHistory from './albumHistoryActions';
|
||||||
import * as albumStudio from './albumStudioActions';
|
|
||||||
import * as app from './appActions';
|
import * as app from './appActions';
|
||||||
import * as artist from './artistActions';
|
import * as artist from './artistActions';
|
||||||
import * as artistHistory from './artistHistoryActions';
|
import * as artistHistory from './artistHistoryActions';
|
||||||
|
@ -46,7 +45,6 @@ export default [
|
||||||
providerOptions,
|
providerOptions,
|
||||||
queue,
|
queue,
|
||||||
releases,
|
releases,
|
||||||
albumStudio,
|
|
||||||
artist,
|
artist,
|
||||||
artistHistory,
|
artistHistory,
|
||||||
artistIndex,
|
artistIndex,
|
||||||
|
|
Loading…
Reference in New Issue