New: Readarr 0.1

pull/15/head
ta264 4 years ago
parent 476f2d6047
commit 08496c82af

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 566 B

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

@ -2,7 +2,7 @@
### Readarr is in early stages of development, alpha/beta binary builds are not yet available. Use of any test builds isn't recommend, and may have detrimental effects on your library.
Readarr is a ebook/magazine/audiobook collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new books from your favorite authors and will grab, sort and rename them.
Readarr is a ebook (and maybe eventually magazine/audiobook) collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new books from your favorite authors and will grab, sort and rename them.
## Major Features Include:
@ -13,7 +13,7 @@ Readarr is a ebook/magazine/audiobook collection manager for Usenet and BitTorre
* Manual search so you can pick any release or to see why a release was not downloaded automatically
* Fully configurable book renaming
* Full integration with SABnzbd and NZBGet
* Full integration with Kodi, Plex (notification, library update, metadata)
* Full integration with Calibre (add to library, conversion)
* And a beautiful UI
## Support

@ -11,7 +11,8 @@ variables:
minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
sentryOrg: 'radarr'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '3.1.102'
trigger:
@ -255,7 +256,6 @@ stages:
sentry-cli releases deploys "${RELEASENAME}" new -e production
fi
displayName: Publish Sentry Source Maps
enabled: false
condition: |
or
(
@ -263,9 +263,9 @@ stages:
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
)
env:
SENTRY_AUTH_TOKEN: $(sentryAuthTokenSelfHosted)
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
SENTRY_ORG: $(sentryOrg)
SENTRY_URL: https://sentry.radarr.video
SENTRY_URL: $(sentryUrl)
- stage: Unit_Test
displayName: Unit Tests
@ -338,15 +338,12 @@ stages:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: false
failTaskOnFailedTests: true
- job: Unit_Docker
displayName: Unit Docker
strategy:
matrix:
mono508:
testName: 'Mono 5.8'
containerImage: lidarr/testimages:mono-5.8
mono520:
testName: 'Mono 5.20'
containerImage: lidarr/testimages:mono-5.20
@ -463,16 +460,13 @@ stages:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Integration Tests'
failTaskOnFailedTests: false
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_Docker
displayName: Integration Docker
strategy:
matrix:
mono508:
testName: 'Mono 5.8'
containerImage: lidarr/testimages:mono-5.8
mono520:
testName: 'Mono 5.20'
containerImage: lidarr/testimages:mono-5.20
@ -557,7 +551,7 @@ stages:
osName: 'Windows'
imageName: 'windows-2019'
pattern: 'Readarr.**.windows-core-x64.zip'
failBuild: true
failBuild: $(failOnAutomationFailure)
pool:
vmImage: $(imageName)
@ -713,3 +707,25 @@ stages:
codeCoverageTool: 'cobertura'
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
reportDirectory: './CoverageResults/combined/'
- stage: Report_Out
dependsOn:
- Analyze
- Unit_Test
- Integration
- Automation
condition: always()
displayName: Build Status Report
jobs:
- job:
displayName: Discord Notification
pool:
vmImage: 'ubuntu-18.04'
steps:
- checkout: none
- powershell: |
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1'))
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey)

2
debian/control vendored

@ -2,7 +2,7 @@ Section: web
Priority: optional
Maintainer: Sonarr <contact@nzbdrone.com>
Source: nzbdrone
Homepage: https://readarr.audio
Homepage: https://readarr.com
Vcs-Git: git@github.com:readarr/Readarr.git
Vcs-Browser: https://github.com/readarr/Readarr

2
debian/copyright vendored

@ -3,7 +3,7 @@ Upstream-Name: nzbdrone
Source: https://github.com/readarr/Readarr
Files: *
Copyright: 2010-2016 Readarr <hello@readarr.audio>
Copyright: 2010-2016 Readarr <hello@readarr.com>
License: GPL-3.0+

@ -1,4 +1,5 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
@ -67,11 +68,11 @@ class BlacklistRow extends Component {
return null;
}
if (name === 'artist.sortName') {
if (name === 'authors.sortName') {
return (
<TableRowCell key={name}>
<ArtistNameLink
foreignArtistId={artist.foreignArtistId}
titleSlug={artist.titleSlug}
artistName={artist.artistName}
/>
</TableRowCell>

@ -61,10 +61,10 @@ class HistoryConnector extends Component {
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
const albumIds = selectUniqueIds(this.props.items, 'albumId');
const bookIds = selectUniqueIds(this.props.items, 'bookId');
const trackIds = selectUniqueIds(this.props.items, 'trackId');
if (albumIds.length) {
this.props.fetchAlbums({ albumIds });
if (bookIds.length) {
this.props.fetchAlbums({ bookIds });
} else {
this.props.clearAlbums();
}

@ -53,7 +53,6 @@ class HistoryRow extends Component {
const {
artist,
album,
track,
quality,
qualityCutoffNotMet,
eventType,
@ -94,22 +93,22 @@ class HistoryRow extends Component {
);
}
if (name === 'artist.sortName') {
if (name === 'authors.sortName') {
return (
<TableRowCell key={name}>
<ArtistNameLink
foreignArtistId={artist.foreignArtistId}
titleSlug={artist.titleSlug}
artistName={artist.artistName}
/>
</TableRowCell>
);
}
if (name === 'album.title') {
if (name === 'books.title') {
return (
<TableRowCell key={name}>
<AlbumTitleLink
foreignAlbumId={album.foreignAlbumId}
titleSlug={album.titleSlug}
title={album.title}
disambiguation={album.disambiguation}
/>
@ -117,14 +116,6 @@ class HistoryRow extends Component {
);
}
if (name === 'trackTitle') {
return (
<TableRowCell key={name}>
{track.title}
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
@ -214,10 +205,9 @@ class HistoryRow extends Component {
}
HistoryRow.propTypes = {
albumId: PropTypes.number,
bookId: PropTypes.number,
artist: PropTypes.object.isRequired,
album: PropTypes.object,
track: PropTypes.object,
quality: PropTypes.object.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired,
eventType: PropTypes.string.isRequired,
@ -232,10 +222,4 @@ HistoryRow.propTypes = {
onMarkAsFailedPress: PropTypes.func.isRequired
};
HistoryRow.defaultProps = {
track: {
title: ''
}
};
export default HistoryRow;

@ -48,7 +48,7 @@ class Queue extends Component {
this.props.isFetching &&
nextProps.isPopulated &&
hasDifferentItems(this.props.items, nextProps.items) &&
nextProps.items.some((e) => e.albumId)
nextProps.items.some((e) => e.bookId)
) {
return false;
}
@ -146,7 +146,7 @@ class Queue extends Component {
} = this.state;
const isRefreshing = isFetching || isAlbumsFetching || isRefreshMonitoredDownloadsExecuting;
const isAllPopulated = isPopulated && (isAlbumsPopulated || !items.length || items.every((e) => !e.albumId));
const isAllPopulated = isPopulated && (isAlbumsPopulated || !items.length || items.every((e) => !e.bookId));
const hasError = error || albumsError;
const selectedIds = this.getSelectedIds();
const selectedCount = selectedIds.length;
@ -236,7 +236,7 @@ class Queue extends Component {
return (
<QueueRowConnector
key={item.id}
albumId={item.albumId}
bookId={item.bookId}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
@ -264,7 +264,7 @@ class Queue extends Component {
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
return !!(item && item.artistId && item.albumId);
return !!(item && item.authorId && item.bookId);
})
)}
onRemovePress={this.onRemoveSelectedConfirmed}

@ -62,10 +62,10 @@ class QueueConnector extends Component {
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
const albumIds = selectUniqueIds(this.props.items, 'albumId');
const bookIds = selectUniqueIds(this.props.items, 'bookId');
if (albumIds.length) {
this.props.fetchAlbums({ albumIds });
if (bookIds.length) {
this.props.fetchAlbums({ bookIds });
} else {
this.props.clearAlbums();
}

@ -137,13 +137,13 @@ class QueueRow extends Component {
);
}
if (name === 'artist.sortName') {
if (name === 'authors.sortName') {
return (
<TableRowCell key={name}>
{
artist ?
<ArtistNameLink
foreignArtistId={artist.foreignArtistId}
titleSlug={artist.titleSlug}
artistName={artist.artistName}
/> :
title
@ -152,13 +152,13 @@ class QueueRow extends Component {
);
}
if (name === 'album.title') {
if (name === 'books.title') {
return (
<TableRowCell key={name}>
{
album ?
<AlbumTitleLink
foreignAlbumId={album.foreignAlbumId}
titleSlug={album.titleSlug}
title={album.title}
disambiguation={album.disambiguation}
/> :
@ -168,7 +168,7 @@ class QueueRow extends Component {
);
}
if (name === 'album.releaseDate') {
if (name === 'books.releaseDate') {
if (album) {
return (
<RelativeDateCellConnector

@ -6,38 +6,38 @@ function ArtistMonitoringOptionsPopoverContent() {
return (
<DescriptionList>
<DescriptionListItem
title="All Albums"
data="Monitor all albums except specials"
title="All Books"
data="Monitor all books"
/>
<DescriptionListItem
title="Future Albums"
data="Monitor albums that have not released yet"
title="Future Books"
data="Monitor books that have not released yet"
/>
<DescriptionListItem
title="Missing Albums"
data="Monitor albums that do not have files or have not released yet"
title="Missing Books"
data="Monitor books that do not have files or have not released yet"
/>
<DescriptionListItem
title="Existing Albums"
data="Monitor albums that have files or have not released yet"
title="Existing Books"
data="Monitor books that have files or have not released yet"
/>
<DescriptionListItem
title="First Album"
data="Monitor the first albums. All other albums will be ignored"
title="First Book"
data="Monitor the first book. All other books will be ignored"
/>
<DescriptionListItem
title="Latest Album"
data="Monitor the latest albums and future albums"
title="Latest Book"
data="Monitor the latest book and future books"
/>
<DescriptionListItem
title="None"
data="No albums will be monitored"
data="No books will be monitored"
/>
</DescriptionList>
);

@ -36,7 +36,7 @@ class AlbumSearchCell extends Component {
render() {
const {
albumId,
bookId,
albumTitle,
isSearching,
onSearchPress,
@ -58,7 +58,7 @@ class AlbumSearchCell extends Component {
<AlbumInteractiveSearchModalConnector
isOpen={this.state.isDetailsModalOpen}
albumId={albumId}
bookId={bookId}
albumTitle={albumTitle}
onModalClose={this.onDetailsModalClose}
{...otherProps}
@ -70,8 +70,8 @@ class AlbumSearchCell extends Component {
}
AlbumSearchCell.propTypes = {
albumId: PropTypes.number.isRequired,
artistId: PropTypes.number.isRequired,
bookId: PropTypes.number.isRequired,
authorId: PropTypes.number.isRequired,
albumTitle: PropTypes.string.isRequired,
isSearching: PropTypes.bool.isRequired,
onSearchPress: PropTypes.func.isRequired

@ -9,10 +9,10 @@ import AlbumSearchCell from './AlbumSearchCell';
function createMapStateToProps() {
return createSelector(
(state, { albumId }) => albumId,
(state, { bookId }) => bookId,
createArtistSelector(),
createCommandsSelector(),
(albumId, artist, commands) => {
(bookId, artist, commands) => {
const isSearching = commands.some((command) => {
const albumSearch = command.name === commandNames.ALBUM_SEARCH;
@ -22,7 +22,7 @@ function createMapStateToProps() {
return (
isCommandExecuting(command) &&
command.body.albumIds.indexOf(albumId) > -1
command.body.bookIds.indexOf(bookId) > -1
);
});
@ -40,7 +40,7 @@ function createMapDispatchToProps(dispatch, props) {
onSearchPress(name, path) {
dispatch(executeCommand({
name: commandNames.ALBUM_SEARCH,
albumIds: [props.albumId]
bookIds: [props.bookId]
}));
}
};

@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
import React from 'react';
import Link from 'Components/Link/Link';
function AlbumTitleLink({ foreignAlbumId, title, disambiguation }) {
const link = `/album/${foreignAlbumId}`;
function AlbumTitleLink({ titleSlug, title, disambiguation }) {
const link = `/book/${titleSlug}`;
return (
<Link to={link}>
@ -13,7 +13,7 @@ function AlbumTitleLink({ foreignAlbumId, title, disambiguation }) {
}
AlbumTitleLink.propTypes = {
foreignAlbumId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
disambiguation: PropTypes.string
};

@ -28,14 +28,14 @@ class DeleteAlbumModalContentConnector extends Component {
onDeletePress = (deleteFiles, addImportListExclusion) => {
this.props.deleteAlbum({
id: this.props.albumId,
id: this.props.bookId,
deleteFiles,
addImportListExclusion
});
this.props.onModalClose(true);
this.props.push(`${window.Readarr.urlBase}/artist/${this.props.foreignArtistId}`);
this.props.push(`${window.Readarr.urlBase}/author/${this.props.titleSlug}`);
}
//
@ -52,8 +52,8 @@ class DeleteAlbumModalContentConnector extends Component {
}
DeleteAlbumModalContentConnector.propTypes = {
albumId: PropTypes.number.isRequired,
foreignArtistId: PropTypes.string.isRequired,
bookId: PropTypes.number.isRequired,
titleSlug: PropTypes.string.isRequired,
push: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired,
deleteAlbum: PropTypes.func.isRequired

@ -35,7 +35,6 @@
.cover {
flex-shrink: 0;
margin-right: 35px;
width: 250px;
height: 250px;
}
@ -136,6 +135,31 @@
padding: 20px;
}
.tabList {
margin: 0;
padding: 0;
border-bottom: 1px solid $lightGray;
}
.tab {
position: relative;
bottom: -1px;
display: inline-block;
padding: 6px 12px;
border: 1px solid transparent;
border-top: none;
list-style: none;
cursor: pointer;
}
.selectedTab {
border-bottom: 4px solid $linkColor;
}
.tabContent {
margin-top: 20px;
}
@media only screen and (max-width: $breakpointSmall) {
.contentContainer {
padding: 20px 0;

@ -2,6 +2,7 @@ import _ from 'lodash';
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import TextTruncate from 'react-text-truncate';
import formatBytes from 'Utilities/Number/formatBytes';
import selectAll from 'Utilities/Table/selectAll';
@ -16,8 +17,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
import Tooltip from 'Components/Tooltip/Tooltip';
import AlbumCover from 'Album/AlbumCover';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
import EditAlbumModalConnector from 'Album/Edit/EditAlbumModalConnector';
// import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
import DeleteAlbumModal from 'Album/Delete/DeleteAlbumModal';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
@ -26,10 +26,10 @@ import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import AlbumDetailsMediumConnector from './AlbumDetailsMediumConnector';
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
import AlbumInteractiveSearchModalConnector from 'Album/Search/AlbumInteractiveSearchModalConnector';
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
import ArtistHistoryTable from 'Artist/History/ArtistHistoryTable';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
import TrackFileEditorTable from 'TrackFile/Editor/TrackFileEditorTable';
import AlbumDetailsLinks from './AlbumDetailsLinks';
import styles from './AlbumDetails.css';
@ -85,14 +85,11 @@ class AlbumDetails extends Component {
this.state = {
isOrganizeModalOpen: false,
isRetagModalOpen: false,
isArtistHistoryModalOpen: false,
isInteractiveSearchModalOpen: false,
isManageTracksOpen: false,
isEditAlbumModalOpen: false,
isDeleteAlbumModalOpen: false,
allExpanded: false,
allCollapsed: false,
expandedState: {}
expandedState: {},
selectedTabIndex: 0
};
}
@ -115,17 +112,8 @@ class AlbumDetails extends Component {
this.setState({ isRetagModalOpen: false });
}
onEditAlbumPress = () => {
this.setState({ isEditAlbumModalOpen: true });
}
onEditAlbumModalClose = () => {
this.setState({ isEditAlbumModalOpen: false });
}
onDeleteAlbumPress = () => {
this.setState({
isEditAlbumModalOpen: false,
isDeleteAlbumModalOpen: true
});
}
@ -134,30 +122,6 @@ class AlbumDetails extends Component {
this.setState({ isDeleteAlbumModalOpen: false });
}
onManageTracksPress = () => {
this.setState({ isManageTracksOpen: true });
}
onManageTracksModalClose = () => {
this.setState({ isManageTracksOpen: false });
}
onInteractiveSearchPress = () => {
this.setState({ isInteractiveSearchModalOpen: true });
}
onInteractiveSearchModalClose = () => {
this.setState({ isInteractiveSearchModalOpen: false });
}
onArtistHistoryPress = () => {
this.setState({ isArtistHistoryModalOpen: true });
}
onArtistHistoryModalClose = () => {
this.setState({ isArtistHistoryModalOpen: false });
}
onExpandAllPress = () => {
const {
allExpanded,
@ -167,7 +131,7 @@ class AlbumDetails extends Component {
this.setState(getExpandedState(selectAll(expandedState, !allExpanded)));
}
onExpandPress = (albumId, isExpanded) => {
onExpandPress = (bookId, isExpanded) => {
this.setState((state) => {
const convertedState = {
allSelected: state.allExpanded,
@ -175,7 +139,7 @@ class AlbumDetails extends Component {
selectedState: state.expandedState
};
const newState = toggleSelected(convertedState, [], albumId, isExpanded, false);
const newState = toggleSelected(convertedState, [], bookId, isExpanded, false);
return getExpandedState(newState);
});
@ -187,23 +151,20 @@ class AlbumDetails extends Component {
render() {
const {
id,
foreignAlbumId,
titleSlug,
title,
disambiguation,
duration,
overview,
albumType,
statistics = {},
monitored,
releaseDate,
ratings,
images,
links,
media,
isSaving,
isFetching,
isPopulated,
albumsError,
trackFilesError,
hasTrackFiles,
shortDateFormat,
@ -217,15 +178,11 @@ class AlbumDetails extends Component {
const {
isOrganizeModalOpen,
isRetagModalOpen,
isArtistHistoryModalOpen,
isInteractiveSearchModalOpen,
isEditAlbumModalOpen,
// isRetagModalOpen,
isDeleteAlbumModalOpen,
isManageTracksOpen,
allExpanded,
allCollapsed,
expandedState
selectedTabIndex
} = this.state;
let expandIcon = icons.EXPAND_INDETERMINATE;
@ -247,12 +204,6 @@ class AlbumDetails extends Component {
onPress={onSearchPress}
/>
<PageToolbarButton
label="Interactive Search"
iconName={icons.INTERACTIVE}
onPress={this.onInteractiveSearchPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
@ -269,27 +220,8 @@ class AlbumDetails extends Component {
onPress={this.onRetagPress}
/>
<PageToolbarButton
label="Manage Tracks"
iconName={icons.TRACK_FILE}
isDisabled={!hasTrackFiles}
onPress={this.onManageTracksPress}
/>
<PageToolbarButton
label="History"
iconName={icons.HISTORY}
onPress={this.onArtistHistoryPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label="Edit"
iconName={icons.EDIT}
onPress={this.onEditAlbumPress}
/>
<PageToolbarButton
label="Delete"
iconName={icons.DELETE}
@ -350,7 +282,7 @@ class AlbumDetails extends Component {
name={icons.ARROW_LEFT}
size={30}
title={`Go to ${previousAlbum.title}`}
to={`/album/${previousAlbum.foreignAlbumId}`}
to={`/book/${previousAlbum.titleSlug}`}
/>
<IconButton
@ -358,7 +290,7 @@ class AlbumDetails extends Component {
name={icons.ARROW_UP}
size={30}
title={`Go to ${artist.artistName}`}
to={`/artist/${artist.foreignArtistId}`}
to={`/author/${artist.titleSlug}`}
/>
<IconButton
@ -366,7 +298,7 @@ class AlbumDetails extends Component {
name={icons.ARROW_RIGHT}
size={30}
title={`Go to ${nextAlbum.title}`}
to={`/album/${nextAlbum.foreignAlbumId}`}
to={`/book/${nextAlbum.titleSlug}`}
/>
</div>
</div>
@ -435,24 +367,6 @@ class AlbumDetails extends Component {
</span>
</Label>
{
!!albumType &&
<Label
className={styles.detailsLabel}
title="Type"
size={sizes.LARGE}
>
<Icon
name={icons.INFO}
size={17}
/>
<span className={styles.qualityProfileName}>
{albumType}
</span>
</Label>
}
<Tooltip
anchor={
<Label
@ -471,7 +385,7 @@ class AlbumDetails extends Component {
}
tooltip={
<AlbumDetailsLinks
foreignAlbumId={foreignAlbumId}
titleSlug={titleSlug}
links={links}
/>
}
@ -483,7 +397,7 @@ class AlbumDetails extends Component {
<div className={styles.overview}>
<TextTruncate
line={Math.floor(125 / (defaultFontSize * lineHeight))}
text={overview}
text={overview.replace(/<[^>]*>?/gm, '')}
/>
</div>
</div>
@ -492,90 +406,92 @@ class AlbumDetails extends Component {
<div className={styles.contentContainer}>
{
!isPopulated && !albumsError && !trackFilesError &&
!isPopulated && !trackFilesError &&
<LoadingIndicator />
}
{
!isFetching && albumsError &&
<div>Loading albums failed</div>
}
{
!isFetching && trackFilesError &&
<div>Loading track files failed</div>
}
{
isPopulated && !!media.length &&
<div>
{
media.slice(0).map((medium) => {
return (
<AlbumDetailsMediumConnector
key={medium.mediumNumber}
albumId={id}
albumMonitored={monitored}
{...medium}
isExpanded={expandedState[medium.mediumNumber]}
onExpandPress={this.onExpandPress}
/>
);
})
}
</div>
<div>Loading book files failed</div>
}
<Tabs selectedIndex={this.state.tabIndex} onSelect={(tabIndex) => this.setState({ selectedTabIndex: tabIndex })}>
<TabList
className={styles.tabList}
>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
History
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
Search
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
Files
</Tab>
{
selectedTabIndex === 1 &&
<div className={styles.filterIcon}>
<InteractiveSearchFilterMenuConnector
type="album"
/>
</div>
}
</TabList>
<TabPanel>
<ArtistHistoryTable
authorId={artist.id}
bookId={id}
/>
</TabPanel>
<TabPanel>
<InteractiveSearchTable
bookId={id}
type="album"
/>
</TabPanel>
<TabPanel>
<TrackFileEditorTable
authorId={artist.id}
bookId={id}
/>
</TabPanel>
</Tabs>
</div>
<OrganizePreviewModalConnector
isOpen={isOrganizeModalOpen}
artistId={artist.id}
albumId={id}
authorId={artist.id}
bookId={id}
onModalClose={this.onOrganizeModalClose}
/>
<RetagPreviewModalConnector
isOpen={isRetagModalOpen}
artistId={artist.id}
albumId={id}
onModalClose={this.onRetagModalClose}
/>
<TrackFileEditorModal
isOpen={isManageTracksOpen}
artistId={artist.id}
albumId={id}
onModalClose={this.onManageTracksModalClose}
/>
<AlbumInteractiveSearchModalConnector
isOpen={isInteractiveSearchModalOpen}
albumId={id}
albumTitle={title}
onModalClose={this.onInteractiveSearchModalClose}
/>
<ArtistHistoryModal
isOpen={isArtistHistoryModalOpen}
artistId={artist.id}
albumId={id}
onModalClose={this.onArtistHistoryModalClose}
/>
<EditAlbumModalConnector
isOpen={isEditAlbumModalOpen}
albumId={id}
artistId={artist.id}
onModalClose={this.onEditAlbumModalClose}
onDeleteArtistPress={this.onDeleteAlbumPress}
/>
{/* <RetagPreviewModalConnector */}
{/* isOpen={isRetagModalOpen} */}
{/* authorId={artist.id} */}
{/* bookId={id} */}
{/* onModalClose={this.onRetagModalClose} */}
{/* /> */}
<DeleteAlbumModal
isOpen={isDeleteAlbumModalOpen}
albumId={id}
foreignArtistId={artist.foreignArtistId}
bookId={id}
titleSlug={artist.titleSlug}
onModalClose={this.onDeleteAlbumModalClose}
/>
@ -587,26 +503,22 @@ class AlbumDetails extends Component {
AlbumDetails.propTypes = {
id: PropTypes.number.isRequired,
foreignAlbumId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
disambiguation: PropTypes.string,
duration: PropTypes.number,
overview: PropTypes.string,
albumType: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
releaseDate: PropTypes.string.isRequired,
ratings: PropTypes.object.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
links: PropTypes.arrayOf(PropTypes.object).isRequired,
media: PropTypes.arrayOf(PropTypes.object).isRequired,
monitored: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired,
isSearching: PropTypes.bool,
isFetching: PropTypes.bool,
isPopulated: PropTypes.bool,
albumsError: PropTypes.object,
tracksError: PropTypes.object,
trackFilesError: PropTypes.object,
hasTrackFiles: PropTypes.bool.isRequired,
artist: PropTypes.object,

@ -8,8 +8,8 @@ import { findCommand, isCommandExecuting } from 'Utilities/Command';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import { toggleAlbumsMonitored } from 'Store/Actions/albumActions';
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions';
import { clearReleases, cancelFetchReleases } from 'Store/Actions/releaseActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import AlbumDetails from './AlbumDetails';
@ -39,18 +39,17 @@ const selectTrackFiles = createSelector(
function createMapStateToProps() {
return createSelector(
(state, { foreignAlbumId }) => foreignAlbumId,
(state) => state.tracks,
(state, { titleSlug }) => titleSlug,
selectTrackFiles,
(state) => state.albums,
createAllArtistSelector(),
createCommandsSelector(),
createUISettingsSelector(),
(foreignAlbumId, tracks, trackFiles, albums, artists, commands, uiSettings) => {
(titleSlug, trackFiles, albums, artists, commands, uiSettings) => {
const sortedAlbums = _.orderBy(albums.items, 'releaseDate');
const albumIndex = _.findIndex(sortedAlbums, { foreignAlbumId });
const albumIndex = _.findIndex(sortedAlbums, { titleSlug });
const album = sortedAlbums[albumIndex];
const artist = _.find(artists, { id: album.artistId });
const artist = _.find(artists, { id: album.authorId });
if (!album) {
return {};
@ -68,12 +67,11 @@ function createMapStateToProps() {
const isSearchingCommand = findCommand(commands, { name: commandNames.ALBUM_SEARCH });
const isSearching = (
isCommandExecuting(isSearchingCommand) &&
isSearchingCommand.body.albumIds.indexOf(album.id) > -1
isSearchingCommand.body.bookIds.indexOf(album.id) > -1
);
const isFetching = tracks.isFetching || isTrackFilesFetching;
const isPopulated = tracks.isPopulated && isTrackFilesPopulated;
const tracksError = tracks.error;
const isFetching = isTrackFilesFetching;
const isPopulated = isTrackFilesPopulated;
return {
...album,
@ -82,7 +80,6 @@ function createMapStateToProps() {
isSearching,
isFetching,
isPopulated,
tracksError,
trackFilesError,
hasTrackFiles,
previousAlbum,
@ -94,17 +91,13 @@ function createMapStateToProps() {
const mapDispatchToProps = {
executeCommand,
fetchTracks,
clearTracks,
fetchTrackFiles,
clearTrackFiles,
clearReleases,
cancelFetchReleases,
toggleAlbumsMonitored
};
function getMonitoredReleases(props) {
return _.map(_.filter(props.releases, { monitored: true }), 'id').sort();
}
class AlbumDetailsConnector extends Component {
componentDidMount() {
@ -113,8 +106,10 @@ class AlbumDetailsConnector extends Component {
}
componentDidUpdate(prevProps) {
if (!_.isEqual(getMonitoredReleases(prevProps), getMonitoredReleases(this.props)) ||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
// If the id has changed we need to clear the albums
// files and fetch from the server.
if (prevProps.id !== this.props.id) {
this.unpopulate();
this.populate();
}
@ -129,14 +124,14 @@ class AlbumDetailsConnector extends Component {
// Control
populate = () => {
const albumId = this.props.id;
const bookId = this.props.id;
this.props.fetchTracks({ albumId });
this.props.fetchTrackFiles({ albumId });
this.props.fetchTrackFiles({ bookId });
}
unpopulate = () => {
this.props.clearTracks();
this.props.cancelFetchReleases();
this.props.clearReleases();
this.props.clearTrackFiles();
}
@ -145,7 +140,7 @@ class AlbumDetailsConnector extends Component {
onMonitorTogglePress = (monitored) => {
this.props.toggleAlbumsMonitored({
albumIds: [this.props.id],
bookIds: [this.props.id],
monitored
});
}
@ -153,7 +148,7 @@ class AlbumDetailsConnector extends Component {
onSearchPress = () => {
this.props.executeCommand({
name: commandNames.ALBUM_SEARCH,
albumIds: [this.props.id]
bookIds: [this.props.id]
});
}
@ -176,11 +171,11 @@ AlbumDetailsConnector.propTypes = {
anyReleaseOk: PropTypes.bool,
isAlbumFetching: PropTypes.bool,
isAlbumPopulated: PropTypes.bool,
foreignAlbumId: PropTypes.string.isRequired,
fetchTracks: PropTypes.func.isRequired,
clearTracks: PropTypes.func.isRequired,
titleSlug: PropTypes.string.isRequired,
fetchTrackFiles: PropTypes.func.isRequired,
clearTrackFiles: PropTypes.func.isRequired,
clearReleases: PropTypes.func.isRequired,
cancelFetchReleases: PropTypes.func.isRequired,
toggleAlbumsMonitored: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};

@ -7,26 +7,12 @@ import styles from './AlbumDetailsLinks.css';
function AlbumDetailsLinks(props) {
const {
foreignAlbumId,
links
} = props;
return (
<div className={styles.links}>
<Link
className={styles.link}
to={`https://musicbrainz.org/release-group/${foreignAlbumId}`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
Musicbrainz
</Label>
</Link>
{links.map((link, index) => {
return (
<span key={index}>
@ -56,7 +42,6 @@ function AlbumDetailsLinks(props) {
}
AlbumDetailsLinks.propTypes = {
foreignAlbumId: PropTypes.string.isRequired,
links: PropTypes.arrayOf(PropTypes.object).isRequired
};

@ -1,210 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds, sizes } from 'Helpers/Props';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TrackRowConnector from './TrackRowConnector';
import styles from './AlbumDetailsMedium.css';
function getMediumStatistics(tracks) {
let trackCount = 0;
let trackFileCount = 0;
let totalTrackCount = 0;
tracks.forEach((track) => {
if (track.trackFileId) {
trackCount++;
trackFileCount++;
} else {
trackCount++;
}
totalTrackCount++;
});
return {
trackCount,
trackFileCount,
totalTrackCount
};
}
function getTrackCountKind(monitored, trackFileCount, trackCount) {
if (trackFileCount === trackCount && trackCount > 0) {
return kinds.SUCCESS;
}
if (!monitored) {
return kinds.WARNING;
}
return kinds.DANGER;
}
class AlbumDetailsMedium extends Component {
//
// Lifecycle
componentDidMount() {
this._expandByDefault();
}
componentDidUpdate(prevProps) {
if (prevProps.albumId !== this.props.albumId) {
this._expandByDefault();
}
}
//
// Control
_expandByDefault() {
const {
mediumNumber,
onExpandPress
} = this.props;
onExpandPress(mediumNumber, mediumNumber === 1);
}
//
// Listeners
onExpandPress = () => {
const {
mediumNumber,
isExpanded
} = this.props;
this.props.onExpandPress(mediumNumber, !isExpanded);
}
//
// Render
render() {
const {
mediumNumber,
mediumFormat,
albumMonitored,
items,
columns,
onTableOptionChange,
isExpanded,
isSmallScreen
} = this.props;
const {
trackCount,
trackFileCount,
totalTrackCount
} = getMediumStatistics(items);
return (
<div
className={styles.medium}
>
<div className={styles.header}>
<div className={styles.left}>
{
<div>
<span className={styles.mediumNumber}>
{mediumFormat} {mediumNumber}
</span>
</div>
}
<Label
title={`${totalTrackCount} tracks total. ${trackFileCount} tracks with files.`}
kind={getTrackCountKind(albumMonitored, trackFileCount, trackCount)}
size={sizes.LARGE}
>
{
<span>{trackFileCount} / {trackCount}</span>
}
</Label>
</div>
<Link
className={styles.expandButton}
onPress={this.onExpandPress}
>
<Icon
className={styles.expandButtonIcon}
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
title={isExpanded ? 'Hide tracks' : 'Show tracks'}
size={24}
/>
{
!isSmallScreen &&
<span>&nbsp;</span>
}
</Link>
</div>
<div>
{
isExpanded &&
<div className={styles.tracks}>
{
items.length ?
<Table
columns={columns}
onTableOptionChange={onTableOptionChange}
>
<TableBody>
{
items.map((item) => {
return (
<TrackRowConnector
key={item.id}
columns={columns}
{...item}
/>
);
})
}
</TableBody>
</Table> :
<div className={styles.noTracks}>
No tracks in this medium
</div>
}
<div className={styles.collapseButtonContainer}>
<IconButton
name={icons.COLLAPSE}
size={20}
title="Hide tracks"
onPress={this.onExpandPress}
/>
</div>
</div>
}
</div>
</div>
);
}
}
AlbumDetailsMedium.propTypes = {
albumId: PropTypes.number.isRequired,
albumMonitored: PropTypes.bool.isRequired,
mediumNumber: PropTypes.number.isRequired,
mediumFormat: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool,
isExpanded: PropTypes.bool,
isSmallScreen: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onExpandPress: PropTypes.func.isRequired
};
export default AlbumDetailsMedium;

@ -1,65 +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 createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import { setTracksTableOption } from 'Store/Actions/trackActions';
import { executeCommand } from 'Store/Actions/commandActions';
import AlbumDetailsMedium from './AlbumDetailsMedium';
function createMapStateToProps() {
return createSelector(
(state, { mediumNumber }) => mediumNumber,
(state) => state.tracks,
createDimensionsSelector(),
(mediumNumber, tracks, dimensions) => {
const tracksInMedium = _.filter(tracks.items, { mediumNumber });
const sortedTracks = _.orderBy(tracksInMedium, ['absoluteTrackNumber'], ['asc']);
return {
items: sortedTracks,
columns: tracks.columns,
isSmallScreen: dimensions.isSmallScreen
};
}
);
}
const mapDispatchToProps = {
setTracksTableOption,
executeCommand
};
class AlbumDetailsMediumConnector extends Component {
//
// Listeners
onTableOptionChange = (payload) => {
this.props.setTracksTableOption(payload);
}
//
// Render
render() {
return (
<AlbumDetailsMedium
{...this.props}
onTableOptionChange={this.onTableOptionChange}
/>
);
}
}
AlbumDetailsMediumConnector.propTypes = {
albumId: PropTypes.number.isRequired,
albumMonitored: PropTypes.bool.isRequired,
mediumNumber: PropTypes.number.isRequired,
setTracksTableOption: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumDetailsMediumConnector);

@ -17,14 +17,14 @@ function createMapStateToProps() {
(state) => state.albums,
(state) => state.artist,
(match, albums, artist) => {
const foreignAlbumId = match.params.foreignAlbumId;
const titleSlug = match.params.titleSlug;
const isFetching = albums.isFetching || artist.isFetching;
const isPopulated = albums.isPopulated && artist.isPopulated;
// if albums have been fetched, make sure requested one exists
// otherwise don't map foreignAlbumId to trigger not found page
// otherwise don't map titleSlug to trigger not found page
if (!isFetching && isPopulated) {
const albumIndex = _.findIndex(albums.items, { foreignAlbumId });
const albumIndex = _.findIndex(albums.items, { titleSlug });
if (albumIndex === -1) {
return {
isFetching,
@ -34,7 +34,7 @@ function createMapStateToProps() {
}
return {
foreignAlbumId,
titleSlug,
isFetching,
isPopulated
};
@ -69,10 +69,10 @@ class AlbumDetailsPageConnector extends Component {
// Control
populate = () => {
const foreignAlbumId = this.props.foreignAlbumId;
const titleSlug = this.props.titleSlug;
this.setState({ hasMounted: true });
this.props.fetchAlbums({
foreignAlbumId,
titleSlug,
includeAllArtistAlbums: true
});
}
@ -86,15 +86,15 @@ class AlbumDetailsPageConnector extends Component {
render() {
const {
foreignAlbumId,
titleSlug,
isFetching,
isPopulated
} = this.props;
if (!foreignAlbumId) {
if (!titleSlug) {
return (
<NotFound
message="Sorry, that album cannot be found."
message="Sorry, that book cannot be found."
/>
);
}
@ -113,7 +113,7 @@ class AlbumDetailsPageConnector extends Component {
if (!isFetching && isPopulated && this.state.hasMounted) {
return (
<AlbumDetailsConnector
foreignAlbumId={foreignAlbumId}
titleSlug={titleSlug}
/>
);
}
@ -121,8 +121,8 @@ class AlbumDetailsPageConnector extends Component {
}
AlbumDetailsPageConnector.propTypes = {
foreignAlbumId: PropTypes.string,
match: PropTypes.shape({ params: PropTypes.shape({ foreignAlbumId: PropTypes.string.isRequired }).isRequired }).isRequired,
titleSlug: PropTypes.string,
match: PropTypes.shape({ params: PropTypes.shape({ titleSlug: PropTypes.string.isRequired }).isRequired }).isRequired,
push: PropTypes.func.isRequired,
fetchAlbums: PropTypes.func.isRequired,
clearAlbums: PropTypes.func.isRequired,

@ -1,6 +0,0 @@
.TrackActionsCell {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px;
white-space: nowrap;
}

@ -1,109 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import FileDetailsModal from 'TrackFile/FileDetailsModal';
import styles from './TrackActionsCell.css';
class TrackActionsCell extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isDetailsModalOpen: false,
isConfirmDeleteModalOpen: false
};
}
//
// Listeners
onDetailsPress = () => {
this.setState({ isDetailsModalOpen: true });
}
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
}
onDeleteFilePress = () => {
this.setState({ isConfirmDeleteModalOpen: true });
}
onConfirmDelete = () => {
this.setState({ isConfirmDeleteModalOpen: false });
this.props.deleteTrackFile({ id: this.props.trackFileId });
}
onConfirmDeleteModalClose = () => {
this.setState({ isConfirmDeleteModalOpen: false });
}
//
// Render
render() {
const {
trackFileId,
trackFilePath
} = this.props;
const {
isDetailsModalOpen,
isConfirmDeleteModalOpen
} = this.state;
return (
<TableRowCell className={styles.TrackActionsCell}>
{
trackFilePath &&
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
/>
}
{
trackFilePath &&
<IconButton
name={icons.DELETE}
onPress={this.onDeleteFilePress}
/>
}
<FileDetailsModal
isOpen={isDetailsModalOpen}
onModalClose={this.onDetailsModalClose}
id={trackFileId}
/>
<ConfirmModal
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}
title="Delete Track File"
message={`Are you sure you want to delete ${trackFilePath}?`}
confirmLabel="Delete"
onConfirm={this.onConfirmDelete}
onCancel={this.onConfirmDeleteModalClose}
/>
</TableRowCell>
);
}
}
TrackActionsCell.propTypes = {
id: PropTypes.number.isRequired,
albumId: PropTypes.number.isRequired,
trackFilePath: PropTypes.string,
trackFileId: PropTypes.number.isRequired,
deleteTrackFile: PropTypes.func.isRequired
};
export default TrackActionsCell;

@ -1,30 +0,0 @@
.title {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
white-space: nowrap;
}
.monitored {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 42px;
}
.trackNumber {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 50px;
}
.audio {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 250px;
}
.duration,
.status {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 100px;
}

@ -1,166 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import EpisodeStatusConnector from 'Album/EpisodeStatusConnector';
import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
import TrackActionsCell from './TrackActionsCell';
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
import styles from './TrackRow.css';
class TrackRow extends Component {
//
// Render
render() {
const {
id,
albumId,
mediumNumber,
trackFileId,
absoluteTrackNumber,
title,
duration,
trackFilePath,
columns,
deleteTrackFile
} = this.props;
return (
<TableRow>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'medium') {
return (
<TableRowCell
key={name}
className={styles.trackNumber}
>
{mediumNumber}
</TableRowCell>
);
}
if (name === 'absoluteTrackNumber') {
return (
<TableRowCell
key={name}
className={styles.trackNumber}
>
{absoluteTrackNumber}
</TableRowCell>
);
}
if (name === 'title') {
return (
<TableRowCell
key={name}
className={styles.title}
>
{title}
</TableRowCell>
);
}
if (name === 'path') {
return (
<TableRowCell key={name}>
{
trackFilePath
}
</TableRowCell>
);
}
if (name === 'duration') {
return (
<TableRowCell
key={name}
className={styles.duration}
>
{
formatTimeSpan(duration)
}
</TableRowCell>
);
}
if (name === 'audioInfo') {
return (
<TableRowCell
key={name}
className={styles.audio}
>
<MediaInfoConnector
type={mediaInfoTypes.AUDIO}
trackFileId={trackFileId}
/>
</TableRowCell>
);
}
if (name === 'status') {
return (
<TableRowCell
key={name}
className={styles.status}
>
<EpisodeStatusConnector
albumId={id}
trackFileId={trackFileId}
/>
</TableRowCell>
);
}
if (name === 'actions') {
return (
<TrackActionsCell
key={name}
albumId={albumId}
id={id}
trackFilePath={trackFilePath}
trackFileId={trackFileId}
deleteTrackFile={deleteTrackFile}
/>
);
}
return null;
})
}
</TableRow>
);
}
}
TrackRow.propTypes = {
deleteTrackFile: PropTypes.func.isRequired,
id: PropTypes.number.isRequired,
albumId: PropTypes.number.isRequired,
trackFileId: PropTypes.number,
mediumNumber: PropTypes.number.isRequired,
trackNumber: PropTypes.string.isRequired,
absoluteTrackNumber: PropTypes.number,
title: PropTypes.string.isRequired,
duration: PropTypes.number.isRequired,
isSaving: PropTypes.bool,
trackFilePath: PropTypes.string,
mediaInfo: PropTypes.object,
columns: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default TrackRow;

@ -1,23 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createTrackFileSelector from 'Store/Selectors/createTrackFileSelector';
import { deleteTrackFile } from 'Store/Actions/trackFileActions';
import TrackRow from './TrackRow';
function createMapStateToProps() {
return createSelector(
(state, { id }) => id,
createTrackFileSelector(),
(id, trackFile) => {
return {
trackFilePath: trackFile ? trackFile.path : null
};
}
);
}
const mapDispatchToProps = {
deleteTrackFile
};
export default connect(createMapStateToProps, mapDispatchToProps)(TrackRow);

@ -1,25 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import EditAlbumModalContentConnector from './EditAlbumModalContentConnector';
function EditAlbumModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditAlbumModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditAlbumModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditAlbumModal;

@ -1,39 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditAlbumModal from './EditAlbumModal';
const mapDispatchToProps = {
clearPendingChanges
};
class EditAlbumModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.clearPendingChanges({ section: 'albums' });
this.props.onModalClose();
}
//
// Render
render() {
return (
<EditAlbumModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
EditAlbumModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(undefined, mapDispatchToProps)(EditAlbumModalConnector);

@ -1,133 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { inputTypes } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
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 Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
class EditAlbumModalContent extends Component {
//
// Listeners
onSavePress = () => {
const {
onSavePress
} = this.props;
onSavePress(false);
}
//
// Render
render() {
const {
title,
artistName,
albumType,
statistics,
item,
isSaving,
onInputChange,
onModalClose,
...otherProps
} = this.props;
const {
monitored,
anyReleaseOk,
releases
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Edit - {artistName} - {title} [{albumType}]
</ModalHeader>
<ModalBody>
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>Monitored</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="monitored"
helpText="Readarr will search for and download album"
{...monitored}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Automatically Switch Release</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="anyReleaseOk"
helpText="Readarr will automatically switch to the release best matching downloaded tracks"
{...anyReleaseOk}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel> Release</FormLabel>
<FormInputGroup
type={inputTypes.ALBUM_RELEASE_SELECT}
name="releases"
helpText="Change release for this album"
isDisabled={anyReleaseOk.value && statistics.trackFileCount > 0}
albumReleases={releases}
onChange={onInputChange}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
Cancel
</Button>
<SpinnerButton
isSpinning={isSaving}
onPress={this.onSavePress}
>
Save
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
}
EditAlbumModalContent.propTypes = {
albumId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired,
albumType: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
item: PropTypes.object.isRequired,
isSaving: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditAlbumModalContent;

@ -1,98 +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 selectSettings from 'Store/Selectors/selectSettings';
import createAlbumSelector from 'Store/Selectors/createAlbumSelector';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import { setAlbumValue, saveAlbum } from 'Store/Actions/albumActions';
import EditAlbumModalContent from './EditAlbumModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.albums,
createAlbumSelector(),
createArtistSelector(),
(albumState, album, artist) => {
const {
isSaving,
saveError,
pendingChanges
} = albumState;
const albumSettings = _.pick(album, [
'monitored',
'anyReleaseOk',
'releases'
]);
const settings = selectSettings(albumSettings, pendingChanges, saveError);
return {
title: album.title,
artistName: artist.artistName,
albumType: album.albumType,
statistics: album.statistics,
isSaving,
saveError,
item: settings.settings,
...settings
};
}
);
}
const mapDispatchToProps = {
dispatchSetAlbumValue: setAlbumValue,
dispatchSaveAlbum: saveAlbum
};
class EditAlbumModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetAlbumValue({ name, value });
}
onSavePress = () => {
this.props.dispatchSaveAlbum({
id: this.props.albumId
});
}
//
// Render
render() {
return (
<EditAlbumModalContent
{...this.props}
onInputChange={this.onInputChange}
onSavePress={this.onSavePress}
/>
);
}
}
EditAlbumModalContentConnector.propTypes = {
albumId: PropTypes.number,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
dispatchSetAlbumValue: PropTypes.func.isRequired,
dispatchSaveAlbum: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditAlbumModalContentConnector);

@ -46,7 +46,7 @@ class EpisodeStatusConnector extends Component {
}
EpisodeStatusConnector.propTypes = {
albumId: PropTypes.number.isRequired,
bookId: PropTypes.number.isRequired,
trackFileId: PropTypes.number.isRequired
};

@ -6,7 +6,7 @@ import AlbumInteractiveSearchModalContent from './AlbumInteractiveSearchModalCon
function AlbumInteractiveSearchModal(props) {
const {
isOpen,
albumId,
bookId,
albumTitle,
onModalClose
} = props;
@ -18,7 +18,7 @@ function AlbumInteractiveSearchModal(props) {
onModalClose={onModalClose}
>
<AlbumInteractiveSearchModalContent
albumId={albumId}
bookId={bookId}
albumTitle={albumTitle}
onModalClose={onModalClose}
/>
@ -28,7 +28,7 @@ function AlbumInteractiveSearchModal(props) {
AlbumInteractiveSearchModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
albumId: PropTypes.number.isRequired,
bookId: PropTypes.number.isRequired,
albumTitle: PropTypes.string.isRequired,
onModalClose: PropTypes.func.isRequired
};

@ -10,7 +10,7 @@ import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConne
function AlbumInteractiveSearchModalContent(props) {
const {
albumId,
bookId,
albumTitle,
onModalClose
} = props;
@ -18,14 +18,14 @@ function AlbumInteractiveSearchModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Interactive Search {albumId != null && `- ${albumTitle}`}
Interactive Search {bookId != null && `- ${albumTitle}`}
</ModalHeader>
<ModalBody scrollDirection={scrollDirections.BOTH}>
<InteractiveSearchConnector
type="album"
searchPayload={{
albumId
bookId
}}
/>
</ModalBody>
@ -40,7 +40,7 @@ function AlbumInteractiveSearchModalContent(props) {
}
AlbumInteractiveSearchModalContent.propTypes = {
albumId: PropTypes.number.isRequired,
bookId: PropTypes.number.isRequired,
albumTitle: PropTypes.string.isRequired,
onModalClose: PropTypes.func.isRequired
};

@ -41,7 +41,7 @@ const columns = [
},
{
name: 'albumCount',
label: 'Albums',
label: 'Books',
isSortable: false,
isVisible: true
}
@ -253,7 +253,7 @@ class AlbumStudio extends Component {
>
<AlbumStudioRowConnector
key={item.id}
artistId={item.id}
authorId={item.id}
isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange}
/>
@ -282,7 +282,7 @@ class AlbumStudio extends Component {
onUpdateSelectedPress = (changes) => {
this.props.onUpdateSelectedPress({
artistIds: this.getSelectedIds(),
authorIds: this.getSelectedIds(),
...changes
});
}

@ -25,7 +25,6 @@ class AlbumStudioAlbum extends Component {
const {
title,
disambiguation,
albumType,
monitored,
statistics,
isSaving
@ -53,14 +52,6 @@ class AlbumStudioAlbum extends Component {
</span>
</div>
<div className={styles.albumType}>
<span>
{
`${albumType}`
}
</span>
</div>
<div
className={classNames(
styles.tracks,
@ -82,7 +73,6 @@ 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,

@ -89,7 +89,7 @@ class AlbumStudioFooter extends Component {
<PageContentFooter>
<div className={styles.inputContainer}>
<div className={styles.label}>
Monitor Artist
Monitor Author
</div>
<SelectInput
@ -103,7 +103,7 @@ class AlbumStudioFooter extends Component {
<div className={styles.inputContainer}>
<div className={styles.label}>
Monitor Albums
Monitor Books
</div>
<MonitorAlbumsSelectInput
@ -117,7 +117,7 @@ class AlbumStudioFooter extends Component {
<div>
<div className={styles.label}>
{selectedCount} Artist(s) Selected
{selectedCount} Author(s) Selected
</div>
<SpinnerButton

@ -16,9 +16,9 @@ class AlbumStudioRow extends Component {
render() {
const {
artistId,
authorId,
status,
foreignArtistId,
titleSlug,
artistName,
monitored,
albums,
@ -33,7 +33,7 @@ class AlbumStudioRow extends Component {
<>
<VirtualTableSelectCell
className={styles.selectCell}
id={artistId}
id={authorId}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
isDisabled={false}
@ -58,7 +58,7 @@ class AlbumStudioRow extends Component {
<VirtualTableRowCell className={styles.title}>
<ArtistNameLink
foreignArtistId={foreignArtistId}
titleSlug={titleSlug}
artistName={artistName}
/>
</VirtualTableRowCell>
@ -82,9 +82,9 @@ class AlbumStudioRow extends Component {
}
AlbumStudioRow.propTypes = {
artistId: PropTypes.number.isRequired,
authorId: PropTypes.number.isRequired,
status: PropTypes.string.isRequired,
foreignArtistId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
albums: PropTypes.arrayOf(PropTypes.object).isRequired,

@ -13,7 +13,7 @@ const getAlbumMap = createSelector(
(state) => state.albums.items,
(albums) => {
return albums.reduce((acc, curr) => {
(acc[curr.artistId] = acc[curr.artistId] || []).push(curr);
(acc[curr.authorId] = acc[curr.authorId] || []).push(curr);
return acc;
}, {});
}
@ -29,7 +29,7 @@ function createMapStateToProps() {
return {
...artist,
artistId: artist.id,
authorId: artist.id,
artistName: artist.artistName,
monitored: artist.monitored,
status: artist.status,
@ -52,20 +52,20 @@ class AlbumStudioRowConnector extends Component {
onArtistMonitoredPress = () => {
const {
artistId,
authorId,
monitored
} = this.props;
this.props.toggleArtistMonitored({
artistId,
authorId,
monitored: !monitored
});
}
onAlbumMonitoredPress = (albumId, monitored) => {
const albumIds = [albumId];
onAlbumMonitoredPress = (bookId, monitored) => {
const bookIds = [bookId];
this.props.toggleAlbumsMonitored({
albumIds,
bookIds,
monitored
});
}
@ -85,7 +85,7 @@ class AlbumStudioRowConnector extends Component {
}
AlbumStudioRowConnector.propTypes = {
artistId: PropTypes.number.isRequired,
authorId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
toggleArtistMonitored: PropTypes.func.isRequired,
toggleAlbumsMonitored: PropTypes.func.isRequired

@ -91,12 +91,12 @@ function AppRoutes(props) {
/>
<Route
path="/artist/:foreignArtistId"
path="/author/:titleSlug"
component={ArtistDetailsPageConnector}
/>
<Route
path="/album/:foreignAlbumId"
path="/book/:titleSlug"
component={AlbumDetailsPageConnector}
/>

@ -1,160 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LazyLoad from 'react-lazyload';
const logoPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPcAAAD3AgMAAAC84irAAAAADFBMVEUyMjI7Ozs1NTU4ODjgOsZvAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+EJEBIzDdm9OfoAAAbkSURBVGje7Zq9b9s4FMBZFgUkBR27C3cw0MromL1jxwyVZASB67G4qWPgoSAyBdm9CwECKCp8nbIccGj/Ce/BTUb3Lh3aI997pCjnTnyyt0JcIif5+ZHvPZLvQ0KMYxzjGMc4xjGOcYxjHOP4JUfSfP7RVPvSH3MYX/eC5aecxne1v+w95WebFs/rwVO/8+h8PnT6t3ln/DFQuJ06/SyHiX9pxa7o5/lewkuLDxLvhM8tPki8g07dU8Gnj5zGlw7P79n4pDVYi8/YuHO4n03z0z6XXDom4G3TXDdN840+LobN/W1Ty2slHD8bNvevlUgutLmTj4NmT3pf6mMGcJGth+gefaZsDCjB2Wj65wN8ZmnAGnE6eFieI1FvcEISLjIUr9hm+w7PFeHiE9t0E7dyIatE48odXTPu0j/A3BMnXf7NXDxudTxbE2VxMWVu+sfwf3i1ZMLiaQLf+iWIP4VtjtTzFhc35vfveZrb4nPt4R95ulu1cxeVh8Psw7rzbgWp8dWHyr83WJpbgjypjS5XeZnqRxmJNUd3MS1d6ue/tOn0WuayNd2CoTlaeqwnIVeOgcWHdHdMS9cSN1vCy3bxZwzFm6VL7QA14WTudVj1sFvf4ReZNSCO0IvwngXFV3hkFcriuPokrPrYbYxjVAHiZ24zLYIeP7/E4xZUgHiZWt29D9ptGemHR7mPo9B10HLGbucRfs/Ww2f2CD4L2u0+wofKwwvrd0XoqCmr38CAZa1d58LesEpvgqtN4MCR1mVj2nZWOiweVB/CAXuyi59Y1auA2eekg6Xw8Tfm013A8LFV8mYXL61ZF4Hb8Zx8d9vBtbdG7s99XvOOZlF38QVtmlkAv0ffxTOjxU/o5p8FvKbSszw2ik87+Iz23Lwf134RiWf2tG3xN2T4oh8vDO4U33z+5qnefFnR77OA2wheh2WfbJBHeI/XgtNJEaHdtJNrvPn8E8eV/kW/2xn8FDc77LemOyq4J1XvSbds7SZ3cAV+86UXP283TGaFUk4ZwmNyugne8FaqxdHtFkH8GNewg2cc3PjsM7CbbNdMwQJ47aL3mP5H308ar5XOn2nUwpx+4hrx/z+qn5DBNqD4rMUpWACnPwnhkfa9SnZwvX1MnHLVi08cPle+0wBuAsykd8dO0KkS9L0dPCO37MVLxJc6nPHdTeNT/ZeLDQN/DEFpBzc33Bfckhx8K1q7IS5vuPgjbTf5AL97zcALxFUHN76QrF7heTHru54RN3bbxTeEn4Xx04f4NOfhSuPLncmnQk3z1yLlSE8fabtFHVyZyIQlXes8zrdSJR5ea7k3+asUooXg2mO4oDprT/XdHpROhouL/8A3edBw5DYxBhYdn08Q53jd0elDfApHbHjL6Hk/pvvNd1rEWdLl9iG+hpMgiMMdVEM64B8X5nq6ZBwX5rCSeK/4uInJROiwetLi0jtpG0yJBPOkTVQXryEPKqMQbq6JeyUTvUOkilq/EVGmo5NIpP3XRIzhXIafrjzF30JUIqecKxIjOpF6il9jbHTLxjs3rN5voPH+GxbDA1m7GrM9a4zdTigdCUUXD2MSSEAXQRxDo2QHl2iwV+h7gchqLrLrhmKxH/Z6nqLUQD5AYSHWAEwk+Z1Ck1vEAmEhBaVtufDtj8Zmv6U+PQNBqbDf/szVR5XNvQteSAzRyeQhzgnIKR2Invq43gQb4+oRaJCTTcRd6RkzGXlJQe3vDq8gsDB2S0QaSoViwKNW9Sh9zUzEMA2MWtU7nJUGYhIa4bnjcLthgkkopMAGj3dxXgoMCbg+laTFL8luSn9pFkrAMf031cmVJz0jXzsKFm6OSfVqYnEILPKZDjeicPFhQoaHbMhKX+NmZ5Q+ntr8n5obhGPVKlx48cs+FteKP3MlswWv6CSPHK4Dmntm0ckreW0snmxKbsnLFdyo4mrwjLYJo+Dmyn0k3uDTEpMRTrnPKza+IHy9wGSEU2yMvSrvHeJ/Qt2UV+p0hVacvsah0psKXqEVy7y2tPu3xhM1oMxLReY00tAlJG9JFZktzCwyU4lbuqQ7U22VN1zi9gvsIP05PjAL7H55H/C6rREzyvu41bbS4VXb1OV0FLG1YVsa1J1gtzaosVJbHO3Gb6z4bR2H89s61FRqCIcgL+E3lfyWlsaN3eR6QDP0pSdeKqOEZjOgoda285SUl5W+Jga181wz0WQFF2poM7FtZTZKXlXZ0Fam10htroY3Ug9s43pN5OJ2jyZy28Iu1nu0sNsGenGzRwO9bd8Xd/u0793LA8Vmn5cHnPhiH+Gt+HIv4Ye+tnHoSyMHvrJy6Aszh76uc+DLQuLQV5XGMY5xjGMc4xjHOMYxjnH80uNfW99BeoyzJCoAAAAASUVORK5CYII=';
function findLogo(images) {
return _.find(images, { coverType: 'logo' });
}
function getLogoUrl(logo, size) {
if (logo) {
// Remove protocol
let url = logo.url.replace(/^https?:/, '');
url = url.replace('logo.jpg', `logo-${size}.jpg`);
return url;
}
}
class ArtistLogo extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const pixelRatio = Math.floor(window.devicePixelRatio);
const {
images,
size
} = props;
const logo = findLogo(images);
this.state = {
pixelRatio,
logo,
logoUrl: getLogoUrl(logo, pixelRatio * size),
hasError: false,
isLoaded: false
};
}
componentDidUpdate(prevProps) {
const {
images,
size
} = this.props;
const {
pixelRatio
} = this.state;
const logo = findLogo(images);
if (logo && logo.url !== this.state.logo.url) {
this.setState({
logo,
logoUrl: getLogoUrl(logo, pixelRatio * size),
hasError: false,
isLoaded: false
});
}
}
//
// Listeners
onError = () => {
this.setState({ hasError: true });
}
onLoad = () => {
this.setState({ isLoaded: true });
}
//
// Render
render() {
const {
className,
style,
size,
lazy,
overflow
} = this.props;
const {
logoUrl,
hasError,
isLoaded
} = this.state;
if (hasError || !logoUrl) {
return (
<img
className={className}
style={style}
src={logoPlaceholder}
/>
);
}
if (lazy) {
return (
<LazyLoad
height={size}
offset={100}
overflow={overflow}
placeholder={
<img
className={className}
style={style}
src={logoPlaceholder}
/>
}
>
<img
className={className}
style={style}
src={logoUrl}
onError={this.onError}
/>
</LazyLoad>
);
}
return (
<img
className={className}
style={style}
src={isLoaded ? logoUrl : logoPlaceholder}
onError={this.onError}
onLoad={this.onLoad}
/>
);
}
}
ArtistLogo.propTypes = {
className: PropTypes.string,
style: PropTypes.object,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
size: PropTypes.number.isRequired,
lazy: PropTypes.bool.isRequired,
overflow: PropTypes.bool.isRequired
};
ArtistLogo.defaultProps = {
size: 250,
lazy: true,
overflow: false
};
export default ArtistLogo;

@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
import React from 'react';
import Link from 'Components/Link/Link';
function ArtistNameLink({ foreignArtistId, artistName }) {
const link = `/artist/${foreignArtistId}`;
function ArtistNameLink({ titleSlug, artistName }) {
const link = `/author/${titleSlug}`;
return (
<Link to={link}>
@ -13,7 +13,7 @@ function ArtistNameLink({ foreignArtistId, artistName }) {
}
ArtistNameLink.propTypes = {
foreignArtistId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired
};

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import ArtistImage from './ArtistImage';
const posterPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPcAAAD3AgMAAAC84irAAAAADFBMVEUyMjI7Ozs1NTU4ODjgOsZvAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+EJEBIzDdm9OfoAAAbkSURBVGje7Zq9b9s4FMBZFgUkBR27C3cw0MromL1jxwyVZASB67G4qWPgoSAyBdm9CwECKCp8nbIccGj/Ce/BTUb3Lh3aI997pCjnTnyyt0JcIif5+ZHvPZLvQ0KMYxzjGMc4xjGOcYxjHOP4JUfSfP7RVPvSH3MYX/eC5aecxne1v+w95WebFs/rwVO/8+h8PnT6t3ln/DFQuJ06/SyHiX9pxa7o5/lewkuLDxLvhM8tPki8g07dU8Gnj5zGlw7P79n4pDVYi8/YuHO4n03z0z6XXDom4G3TXDdN840+LobN/W1Ty2slHD8bNvevlUgutLmTj4NmT3pf6mMGcJGth+gefaZsDCjB2Wj65wN8ZmnAGnE6eFieI1FvcEISLjIUr9hm+w7PFeHiE9t0E7dyIatE48odXTPu0j/A3BMnXf7NXDxudTxbE2VxMWVu+sfwf3i1ZMLiaQLf+iWIP4VtjtTzFhc35vfveZrb4nPt4R95ulu1cxeVh8Psw7rzbgWp8dWHyr83WJpbgjypjS5XeZnqRxmJNUd3MS1d6ue/tOn0WuayNd2CoTlaeqwnIVeOgcWHdHdMS9cSN1vCy3bxZwzFm6VL7QA14WTudVj1sFvf4ReZNSCO0IvwngXFV3hkFcriuPokrPrYbYxjVAHiZ24zLYIeP7/E4xZUgHiZWt29D9ptGemHR7mPo9B10HLGbucRfs/Ww2f2CD4L2u0+wofKwwvrd0XoqCmr38CAZa1d58LesEpvgqtN4MCR1mVj2nZWOiweVB/CAXuyi59Y1auA2eekg6Xw8Tfm013A8LFV8mYXL61ZF4Hb8Zx8d9vBtbdG7s99XvOOZlF38QVtmlkAv0ffxTOjxU/o5p8FvKbSszw2ik87+Iz23Lwf134RiWf2tG3xN2T4oh8vDO4U33z+5qnefFnR77OA2wheh2WfbJBHeI/XgtNJEaHdtJNrvPn8E8eV/kW/2xn8FDc77LemOyq4J1XvSbds7SZ3cAV+86UXP283TGaFUk4ZwmNyugne8FaqxdHtFkH8GNewg2cc3PjsM7CbbNdMwQJ47aL3mP5H308ar5XOn2nUwpx+4hrx/z+qn5DBNqD4rMUpWACnPwnhkfa9SnZwvX1MnHLVi08cPle+0wBuAsykd8dO0KkS9L0dPCO37MVLxJc6nPHdTeNT/ZeLDQN/DEFpBzc33Bfckhx8K1q7IS5vuPgjbTf5AL97zcALxFUHN76QrF7heTHru54RN3bbxTeEn4Xx04f4NOfhSuPLncmnQk3z1yLlSE8fabtFHVyZyIQlXes8zrdSJR5ea7k3+asUooXg2mO4oDprT/XdHpROhouL/8A3edBw5DYxBhYdn08Q53jd0elDfApHbHjL6Hk/pvvNd1rEWdLl9iG+hpMgiMMdVEM64B8X5nq6ZBwX5rCSeK/4uInJROiwetLi0jtpG0yJBPOkTVQXryEPKqMQbq6JeyUTvUOkilq/EVGmo5NIpP3XRIzhXIafrjzF30JUIqecKxIjOpF6il9jbHTLxjs3rN5voPH+GxbDA1m7GrM9a4zdTigdCUUXD2MSSEAXQRxDo2QHl2iwV+h7gchqLrLrhmKxH/Z6nqLUQD5AYSHWAEwk+Z1Ck1vEAmEhBaVtufDtj8Zmv6U+PQNBqbDf/szVR5XNvQteSAzRyeQhzgnIKR2Invq43gQb4+oRaJCTTcRd6RkzGXlJQe3vDq8gsDB2S0QaSoViwKNW9Sh9zUzEMA2MWtU7nJUGYhIa4bnjcLthgkkopMAGj3dxXgoMCbg+laTFL8luSn9pFkrAMf031cmVJz0jXzsKFm6OSfVqYnEILPKZDjeicPFhQoaHbMhKX+NmZ5Q+ntr8n5obhGPVKlx48cs+FteKP3MlswWv6CSPHK4Dmntm0ckreW0snmxKbsnLFdyo4mrwjLYJo+Dmyn0k3uDTEpMRTrnPKza+IHy9wGSEU2yMvSrvHeJ/Qt2UV+p0hVacvsah0psKXqEVy7y2tPu3xhM1oMxLReY00tAlJG9JFZktzCwyU4lbuqQ7U22VN1zi9gvsIP05PjAL7H55H/C6rREzyvu41bbS4VXb1OV0FLG1YVsa1J1gtzaosVJbHO3Gb6z4bR2H89s61FRqCIcgL+E3lfyWlsaN3eR6QDP0pSdeKqOEZjOgoda285SUl5W+Jga181wz0WQFF2poM7FtZTZKXlXZ0Fam10htroY3Ug9s43pN5OJ2jyZy28Iu1nu0sNsGenGzRwO9bd8Xd/u0793LA8Vmn5cHnPhiH+Gt+HIv4Ye+tnHoSyMHvrJy6Aszh76uc+DLQuLQV5XGMY5xjGMc4xjHOMYxjnH80uNfW99BeoyzJCoAAAAASUVORK5CYII=';
const posterPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPcAAAD3AQMAAAD7QlAQAAAABlBMVEUnJychISEIs8G4AAAEFklEQVRYw+2YMdPOQBDH95KQDEVSMKOj1FFQJx9BQc0nkeuUvoLS+AQ6MQqlRu8xCjpUgsjK7iXz2t1LMsbo8i/ey5vfc3e7m7tk9+DQoUOHDpGe3bu7hS8BwJ117BoAOLfOb/Hf62s4EY1VNrcPVvjNua1WuJ/b8xqoeR3sqFkllx8+AYAra9PniDg1ydr07cT7FQMy6k7ycQMKgJr5F4BrhvI9ZA3xCDU8fJggs9gBXJ35acX8lil74CPmO5w1xhwoIMVFMQcqKCfynH3soLLuEfkB4O5TBArDPZlH05ZkYMxBigyJDEyseylHFjjK4CzPyS4IE3gTgIxuAyulHzbG/as0PYsifM24X8/TA19Vxn2efjagNwFoHE2/GDAKpm86HE2AfMrmLQbqADnI2bzFQPv8y7NlM7naORU+uid+62X4xJg0V6PC1+KfvvSghWMgnh0cVIArCO694Ib+qWR4HQ257F9oRxu+L2FpzK3h7D5vPwqA5k1OPOwA4iaAOYWnZM4XPhPYT3eWDXriX4sHROjpskF7cC2eBHfUdVjeDw6/4Uk9oHqEz18DH9se8IvgCdQDBS/oLUxcPcB24mnAv+jfXvCMOdwI9jNXDxiJp9w9DCd4Afgdz96fF5GGk3xSCFBHw+gF4PAz9SQCwE7K5UGculJHGuTdKPun+IYHrafAUPfPKJdP4OhL7ErDuf9jfnXn6Gu6+Kj654EPKQIG7iu5PMLacGPO7Qf0EOMvx3LhhRh/5l+GOsahnPkw4Mw7sXzLedzxV+DvscsMZ8X51W0Olp/+5P7qIPlLPMEWP+3z5G94rXinuen/RWzAbe6g7hVvRX/DO8FdjMPB9+O3yD5fwf1fc72+/jcfN/cHRPZPJva/7q/27z9zlPyVfL9Abrgv/oW/Nvyx5vL9rbl5f78R/I3iTnP7fRH83QjVDpfCb4Kr71uxz1FzkN9nxfX32XKVHyj+BfweV/mJkM5Pdnkpsc6PfK64BynDM8lTiU1+l+LPP2iLUJj8sj5z3uaXgMPZFDY/rQDHs/rLTRxMfkwx4mX4hPLjaza/TgIfI/l1xvl5y/wT5+dSCd8rmXf8W2/qgx5S5rRYvAMlri+Ic2MKME9FCdQT/wJ8Ga1vSnzE+Z3l06REJi7qI1VfOXw0xusrCPVZ+6aP12dFqO/qN6d4fZeF+rB804X6sInXl/lrT1vBFtAu1KcuCfWpi9e33VLfJjZAS33ckvlZpH4uedu2nOcWhleiPr9peLFT32fyfGD7fMGBlf/jfCLZOd8oIrw6q4/o2jogzlc2z2fAW8w2nwvd3eqp0YXxCcdiS1HzRC8fw2ezJjvHVtn2tPbhqnOzTgNp1/kdv6pV7ig4RQOruuDBCax1+94dOHTo0KFDk34DoJynpPus3GIAAAAASUVORK5CYII=';
function ArtistPoster(props) {
return (

@ -26,7 +26,7 @@ class DeleteArtistModalContentConnector extends Component {
onDeletePress = (deleteFiles, addImportListExclusion) => {
this.props.deleteArtist({
id: this.props.artistId,
id: this.props.authorId,
deleteFiles,
addImportListExclusion
});
@ -48,7 +48,7 @@ class DeleteArtistModalContentConnector extends Component {
}
DeleteArtistModalContentConnector.propTypes = {
artistId: PropTypes.number.isRequired,
authorId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired,
deleteArtist: PropTypes.func.isRequired
};

@ -6,7 +6,6 @@ import { kinds, sizes } from 'Helpers/Props';
import TableRow from 'Components/Table/TableRow';
import Label from 'Components/Label';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import AlbumSearchCellConnector from 'Album/AlbumSearchCellConnector';
import AlbumTitleLink from 'Album/AlbumTitleLink';
import StarRating from 'Components/StarRating';
@ -67,19 +66,17 @@ class AlbumRow extends Component {
render() {
const {
id,
artistId,
authorId,
monitored,
statistics,
duration,
releaseDate,
mediumCount,
secondaryTypes,
title,
position,
ratings,
disambiguation,
isSaving,
artistMonitored,
foreignAlbumId,
titleSlug,
columns
} = this.props;
@ -125,7 +122,7 @@ class AlbumRow extends Component {
className={styles.title}
>
<AlbumTitleLink
foreignAlbumId={foreignAlbumId}
titleSlug={titleSlug}
title={title}
disambiguation={disambiguation}
/>
@ -133,42 +130,13 @@ class AlbumRow extends Component {
);
}
if (name === 'mediumCount') {
if (name === 'position') {
return (
<TableRowCell key={name}>
{
mediumCount
}
</TableRowCell>
);
}
if (name === 'secondaryTypes') {
return (
<TableRowCell key={name}>
{
secondaryTypes
}
</TableRowCell>
);
}
if (name === 'trackCount') {
return (
<TableRowCell key={name}>
{
statistics.totalTrackCount
}
</TableRowCell>
);
}
if (name === 'duration') {
return (
<TableRowCell key={name}>
{
formatTimeSpan(duration)
}
<TableRowCell
key={name}
className={styles.title}
>
{position || ''}
</TableRowCell>
);
}
@ -218,8 +186,8 @@ class AlbumRow extends Component {
return (
<AlbumSearchCellConnector
key={name}
albumId={id}
artistId={artistId}
bookId={id}
authorId={authorId}
albumTitle={title}
/>
);
@ -234,21 +202,17 @@ class AlbumRow extends Component {
AlbumRow.propTypes = {
id: PropTypes.number.isRequired,
artistId: PropTypes.number.isRequired,
authorId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
releaseDate: PropTypes.string.isRequired,
mediumCount: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
releaseDate: PropTypes.string,
title: PropTypes.string.isRequired,
position: PropTypes.string,
ratings: PropTypes.object.isRequired,
disambiguation: PropTypes.string,
secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
foreignAlbumId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
isSaving: PropTypes.bool,
unverifiedSceneNumbering: PropTypes.bool,
artistMonitored: PropTypes.bool.isRequired,
statistics: PropTypes.object.isRequired,
mediaInfo: PropTypes.object,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onMonitorAlbumPress: PropTypes.func.isRequired
};

@ -11,7 +11,6 @@ function createMapStateToProps() {
createTrackFileSelector(),
(artist = {}, trackFile) => {
return {
foreignArtistId: artist.foreignArtistId,
artistMonitored: artist.monitored,
trackFilePath: trackFile ? trackFile.path : null
};

@ -41,7 +41,6 @@
.poster {
flex-shrink: 0;
margin-right: 35px;
width: 250px;
height: 250px;
}
@ -96,6 +95,10 @@
margin-left: 20px;
}
.filterIcon {
float: right;
}
.artistNavigationButtons {
white-space: nowrap;
}
@ -150,6 +153,31 @@
padding: 20px;
}
.tabList {
margin: 0;
padding: 0;
border-bottom: 1px solid $lightGray;
}
.tab {
position: relative;
bottom: -1px;
display: inline-block;
padding: 6px 12px;
border: 1px solid transparent;
border-top: none;
list-style: none;
cursor: pointer;
}
.selectedTab {
border-bottom: 4px solid $linkColor;
}
.tabContent {
margin-top: 20px;
}
@media only screen and (max-width: $breakpointSmall) {
.contentContainer {
padding: 20px 0;

@ -1,6 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import TextTruncate from 'react-text-truncate';
import formatBytes from 'Utilities/Number/formatBytes';
import selectAll from 'Utilities/Table/selectAll';
@ -21,21 +22,23 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
import TrackFileEditorTable from 'TrackFile/Editor/TrackFileEditorTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import ArtistPoster from 'Artist/ArtistPoster';
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
import ArtistHistoryTable from 'Artist/History/ArtistHistoryTable';
import ArtistAlternateTitles from './ArtistAlternateTitles';
import ArtistDetailsSeasonConnector from './ArtistDetailsSeasonConnector';
import AuthorDetailsSeriesConnector from './AuthorDetailsSeriesConnector';
import ArtistTagsConnector from './ArtistTagsConnector';
import ArtistDetailsLinks from './ArtistDetailsLinks';
import styles from './ArtistDetails.css';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
import ArtistInteractiveSearchModalConnector from 'Artist/Search/ArtistInteractiveSearchModalConnector';
import Link from 'Components/Link/Link';
const defaultFontSize = parseInt(fonts.defaultFontSize);
@ -68,15 +71,13 @@ class ArtistDetails extends Component {
this.state = {
isOrganizeModalOpen: false,
isRetagModalOpen: false,
isManageTracksOpen: false,
isEditArtistModalOpen: false,
isDeleteArtistModalOpen: false,
isArtistHistoryModalOpen: false,
isInteractiveImportModalOpen: false,
isInteractiveSearchModalOpen: false,
allExpanded: false,
allCollapsed: false,
expandedState: {}
expandedState: {},
selectedTabIndex: 0
};
}
@ -99,14 +100,6 @@ class ArtistDetails extends Component {
this.setState({ isRetagModalOpen: false });
}
onManageTracksPress = () => {
this.setState({ isManageTracksOpen: true });
}
onManageTracksModalClose = () => {
this.setState({ isManageTracksOpen: false });
}
onInteractiveImportPress = () => {
this.setState({ isInteractiveImportModalOpen: true });
}
@ -115,14 +108,6 @@ class ArtistDetails extends Component {
this.setState({ isInteractiveImportModalOpen: false });
}
onInteractiveSearchPress = () => {
this.setState({ isInteractiveSearchModalOpen: true });
}
onInteractiveSearchModalClose = () => {
this.setState({ isInteractiveSearchModalOpen: false });
}
onEditArtistPress = () => {
this.setState({ isEditArtistModalOpen: true });
}
@ -142,14 +127,6 @@ class ArtistDetails extends Component {
this.setState({ isDeleteArtistModalOpen: false });
}
onArtistHistoryPress = () => {
this.setState({ isArtistHistoryModalOpen: true });
}
onArtistHistoryModalClose = () => {
this.setState({ isArtistHistoryModalOpen: false });
}
onExpandAllPress = () => {
const {
allExpanded,
@ -159,7 +136,7 @@ class ArtistDetails extends Component {
this.setState(getExpandedState(selectAll(expandedState, !allExpanded)));
}
onExpandPress = (albumId, isExpanded) => {
onExpandPress = (bookId, isExpanded) => {
this.setState((state) => {
const convertedState = {
allSelected: state.allExpanded,
@ -167,7 +144,7 @@ class ArtistDetails extends Component {
selectedState: state.expandedState
};
const newState = toggleSelected(convertedState, [], albumId, isExpanded, false);
const newState = toggleSelected(convertedState, [], bookId, isExpanded, false);
return getExpandedState(newState);
});
@ -179,14 +156,12 @@ class ArtistDetails extends Component {
render() {
const {
id,
foreignArtistId,
artistName,
ratings,
path,
statistics,
qualityProfileId,
monitored,
albumTypes,
status,
overview,
links,
@ -203,6 +178,8 @@ class ArtistDetails extends Component {
trackFilesError,
hasAlbums,
hasMonitoredAlbums,
hasSeries,
series,
hasTrackFiles,
previousArtist,
nextArtist,
@ -219,15 +196,13 @@ class ArtistDetails extends Component {
const {
isOrganizeModalOpen,
isRetagModalOpen,
isManageTracksOpen,
isEditArtistModalOpen,
isDeleteArtistModalOpen,
isArtistHistoryModalOpen,
isInteractiveImportModalOpen,
isInteractiveSearchModalOpen,
allExpanded,
allCollapsed,
expandedState
expandedState,
selectedTabIndex
} = this.state;
const continuing = status === 'continuing';
@ -271,15 +246,6 @@ class ArtistDetails extends Component {
onPress={onSearchPress}
/>
<PageToolbarButton
label="Interactive Search"
iconName={icons.INTERACTIVE}
isDisabled={!monitored || !hasMonitoredAlbums || !hasAlbums}
isSpinning={isSearching}
title={hasMonitoredAlbums ? undefined : 'No monitored albums for this artist'}
onPress={this.onInteractiveSearchPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
@ -289,26 +255,12 @@ class ArtistDetails extends Component {
onPress={this.onOrganizePress}
/>
<PageToolbarButton
label="Preview Retag"
iconName={icons.RETAG}
isDisabled={!hasTrackFiles}
onPress={this.onRetagPress}
/>
<PageToolbarButton
label="Manage Tracks"
iconName={icons.TRACK_FILE}
isDisabled={!hasTrackFiles}
onPress={this.onManageTracksPress}
/>
<PageToolbarButton
label="History"
iconName={icons.HISTORY}
isDisabled={!hasAlbums}
onPress={this.onArtistHistoryPress}
/>
{/* <PageToolbarButton */}
{/* label="Preview Retag" */}
{/* iconName={icons.RETAG} */}
{/* isDisabled={!hasTrackFiles} */}
{/* onPress={this.onRetagPress} */}
{/* /> */}
<PageToolbarButton
label="Manual Import"
@ -400,7 +352,7 @@ class ArtistDetails extends Component {
name={icons.ARROW_LEFT}
size={30}
title={`Go to ${previousArtist.artistName}`}
to={`/artist/${previousArtist.foreignArtistId}`}
to={`/author/${previousArtist.titleSlug}`}
/>
<IconButton
@ -416,7 +368,7 @@ class ArtistDetails extends Component {
name={icons.ARROW_RIGHT}
size={30}
title={`Go to ${nextArtist.artistName}`}
to={`/artist/${nextArtist.foreignArtistId}`}
to={`/author/${nextArtist.titleSlug}`}
/>
</div>
</div>
@ -528,7 +480,6 @@ class ArtistDetails extends Component {
}
tooltip={
<ArtistDetailsLinks
foreignArtistId={foreignArtistId}
links={links}
/>
}
@ -554,7 +505,7 @@ class ArtistDetails extends Component {
</span>
</Label>
}
tooltip={<ArtistTagsConnector artistId={id} />}
tooltip={<ArtistTagsConnector authorId={id} />}
kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM}
/>
@ -564,7 +515,7 @@ class ArtistDetails extends Component {
<div className={styles.overview}>
<TextTruncate
line={Math.floor(125 / (defaultFontSize * lineHeight))}
text={overview}
text={overview.replace(/<[^>]*>?/gm, '')}
/>
</div>
</div>
@ -588,30 +539,110 @@ class ArtistDetails extends Component {
}
{
isPopulated && !!albumTypes.length &&
<div>
{
albumTypes.slice(0).map((albumType) => {
return (
<ArtistDetailsSeasonConnector
key={albumType}
artistId={id}
name={albumType}
label={albumType}
{...albumType}
isExpanded={expandedState[albumType]}
onExpandPress={this.onExpandPress}
/>
);
})
}
</div>
isPopulated &&
<Tabs selectedIndex={this.state.tabIndex} onSelect={(tabIndex) => this.setState({ selectedTabIndex: tabIndex })}>
<TabList
className={styles.tabList}
>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
Books
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
Series
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
History
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
Search
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
Files
</Tab>
{
selectedTabIndex === 3 &&
<div className={styles.filterIcon}>
<InteractiveSearchFilterMenuConnector
type="artist"
/>
</div>
}
</TabList>
<TabPanel>
<ArtistDetailsSeasonConnector
authorId={id}
isExpanded={true}
onExpandPress={this.onExpandPress}
/>
</TabPanel>
<TabPanel>
{
isPopulated && hasSeries &&
<div>
{
series.map((item) => {
return (
<AuthorDetailsSeriesConnector
key={item.id}
seriesId={item.id}
authorId={id}
isExpanded={expandedState[item.id]}
onExpandPress={this.onExpandPress}
/>
);
})
}
</div>
}
</TabPanel>
<TabPanel>
<ArtistHistoryTable
authorId={id}
/>
</TabPanel>
<TabPanel>
<InteractiveSearchTable
type="artist"
authorId={id}
/>
</TabPanel>
<TabPanel>
<TrackFileEditorTable
authorId={id}
/>
</TabPanel>
</Tabs>
}
</div>
<div className={styles.metadataMessage}>
Missing Albums, Singles, or Other Types? Modify or create a new
Missing or too many books? Modify or create a new
<Link to='/settings/profiles'> Metadata Profile </Link>
or manually
<Link to={`/add/search?term=${encodeURIComponent(artistName)}`}> Search </Link>
@ -620,38 +651,26 @@ class ArtistDetails extends Component {
<OrganizePreviewModalConnector
isOpen={isOrganizeModalOpen}
artistId={id}
authorId={id}
onModalClose={this.onOrganizeModalClose}
/>
<RetagPreviewModalConnector
isOpen={isRetagModalOpen}
artistId={id}
authorId={id}
onModalClose={this.onRetagModalClose}
/>
<TrackFileEditorModal
isOpen={isManageTracksOpen}
artistId={id}
onModalClose={this.onManageTracksModalClose}
/>
<ArtistHistoryModal
isOpen={isArtistHistoryModalOpen}
artistId={id}
onModalClose={this.onArtistHistoryModalClose}
/>
<EditArtistModalConnector
isOpen={isEditArtistModalOpen}
artistId={id}
authorId={id}
onModalClose={this.onEditArtistModalClose}
onDeleteArtistPress={this.onDeleteArtistPress}
/>
<DeleteArtistModal
isOpen={isDeleteArtistModalOpen}
artistId={id}
authorId={id}
onModalClose={this.onDeleteArtistModalClose}
/>
@ -663,12 +682,6 @@ class ArtistDetails extends Component {
showImportMode={false}
onModalClose={this.onInteractiveImportModalClose}
/>
<ArtistInteractiveSearchModalConnector
isOpen={isInteractiveSearchModalOpen}
artistId={id}
onModalClose={this.onInteractiveSearchModalClose}
/>
</PageContentBodyConnector>
</PageContent>
);
@ -677,7 +690,6 @@ class ArtistDetails extends Component {
ArtistDetails.propTypes = {
id: PropTypes.number.isRequired,
foreignArtistId: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired,
ratings: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
@ -685,7 +697,6 @@ ArtistDetails.propTypes = {
qualityProfileId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
artistType: PropTypes.string,
albumTypes: PropTypes.arrayOf(PropTypes.string),
status: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired,
links: PropTypes.arrayOf(PropTypes.object).isRequired,
@ -701,6 +712,8 @@ ArtistDetails.propTypes = {
trackFilesError: PropTypes.object,
hasAlbums: PropTypes.bool.isRequired,
hasMonitoredAlbums: PropTypes.bool.isRequired,
hasSeries: PropTypes.bool.isRequired,
series: PropTypes.arrayOf(PropTypes.object).isRequired,
hasTrackFiles: PropTypes.bool.isRequired,
previousArtist: PropTypes.object.isRequired,
nextArtist: PropTypes.object.isRequired,

@ -6,12 +6,15 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { findCommand, isCommandExecuting } from 'Utilities/Command';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import { fetchAlbums, clearAlbums } from 'Store/Actions/albumActions';
import { fetchSeries, clearSeries } from 'Store/Actions/seriesActions';
import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions';
import { toggleArtistMonitored } from 'Store/Actions/artistActions';
import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions';
import { clearReleases, cancelFetchReleases } from 'Store/Actions/releaseActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import ArtistDetails from './ArtistDetails';
@ -28,15 +31,36 @@ const selectAlbums = createSelector(
const hasAlbums = !!items.length;
const hasMonitoredAlbums = items.some((e) => e.monitored);
const albumTypes = _.uniq(_.map(items, 'albumType'));
return {
isAlbumsFetching: isFetching,
isAlbumsPopulated: isPopulated,
albumsError: error,
hasAlbums,
hasMonitoredAlbums,
albumTypes
hasMonitoredAlbums
};
}
);
const selectSeries = createSelector(
createSortedSectionSelector('series', (a, b) => a.title.localeCompare(b.title)),
(state) => state.series,
(series) => {
const {
items,
isFetching,
isPopulated,
error
} = series;
const hasSeries = !!items.length;
return {
isSeriesFetching: isFetching,
isSeriesPopulated: isPopulated,
seriesError: error,
hasSeries,
series: series.items
};
}
);
@ -64,14 +88,15 @@ const selectTrackFiles = createSelector(
function createMapStateToProps() {
return createSelector(
(state, { foreignArtistId }) => foreignArtistId,
(state, { titleSlug }) => titleSlug,
selectAlbums,
selectSeries,
selectTrackFiles,
createAllArtistSelector(),
createCommandsSelector(),
(foreignArtistId, albums, trackFiles, allArtists, commands) => {
(titleSlug, albums, series, trackFiles, allArtists, commands) => {
const sortedArtist = _.orderBy(allArtists, 'sortName');
const artistIndex = _.findIndex(sortedArtist, { foreignArtistId });
const artistIndex = _.findIndex(sortedArtist, { titleSlug });
const artist = sortedArtist[artistIndex];
if (!artist) {
@ -83,10 +108,17 @@ function createMapStateToProps() {
isAlbumsPopulated,
albumsError,
hasAlbums,
hasMonitoredAlbums,
albumTypes
hasMonitoredAlbums
} = albums;
const {
isSeriesFetching,
isSeriesPopulated,
seriesError,
hasSeries,
series: seriesItems
} = series;
const {
isTrackFilesFetching,
isTrackFilesPopulated,
@ -94,28 +126,26 @@ function createMapStateToProps() {
hasTrackFiles
} = trackFiles;
const sortedAlbumTypes = _.orderBy(albumTypes);
const previousArtist = sortedArtist[artistIndex - 1] || _.last(sortedArtist);
const nextArtist = sortedArtist[artistIndex + 1] || _.first(sortedArtist);
const isArtistRefreshing = isCommandExecuting(findCommand(commands, { name: commandNames.REFRESH_ARTIST, artistId: artist.id }));
const isArtistRefreshing = isCommandExecuting(findCommand(commands, { name: commandNames.REFRESH_ARTIST, authorId: artist.id }));
const artistRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_ARTIST });
const allArtistRefreshing = (
isCommandExecuting(artistRefreshingCommand) &&
!artistRefreshingCommand.body.artistId
!artistRefreshingCommand.body.authorId
);
const isRefreshing = isArtistRefreshing || allArtistRefreshing;
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.ARTIST_SEARCH, artistId: artist.id }));
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id }));
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.ARTIST_SEARCH, authorId: artist.id }));
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: artist.id }));
const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST });
const isRenamingArtist = (
isCommandExecuting(isRenamingArtistCommand) &&
isRenamingArtistCommand.body.artistIds.indexOf(artist.id) > -1
isRenamingArtistCommand.body.authorIds.indexOf(artist.id) > -1
);
const isFetching = isAlbumsFetching || isTrackFilesFetching;
const isPopulated = isAlbumsPopulated && isTrackFilesPopulated;
const isFetching = isAlbumsFetching || isSeriesFetching || isTrackFilesFetching;
const isPopulated = isAlbumsPopulated && isSeriesPopulated && isTrackFilesPopulated;
const alternateTitles = _.reduce(artist.alternateTitles, (acc, alternateTitle) => {
if ((alternateTitle.seasonNumber === -1 || alternateTitle.seasonNumber === undefined) &&
@ -128,7 +158,6 @@ function createMapStateToProps() {
return {
...artist,
albumTypes: sortedAlbumTypes,
alternateTitles,
isArtistRefreshing,
allArtistRefreshing,
@ -139,9 +168,12 @@ function createMapStateToProps() {
isFetching,
isPopulated,
albumsError,
seriesError,
trackFilesError,
hasAlbums,
hasMonitoredAlbums,
hasSeries,
series: seriesItems,
hasTrackFiles,
previousArtist,
nextArtist
@ -153,11 +185,15 @@ function createMapStateToProps() {
const mapDispatchToProps = {
fetchAlbums,
clearAlbums,
fetchSeries,
clearSeries,
fetchTrackFiles,
clearTrackFiles,
toggleArtistMonitored,
fetchQueueDetails,
clearQueueDetails,
clearReleases,
cancelFetchReleases,
executeCommand
};
@ -207,17 +243,21 @@ class ArtistDetailsConnector extends Component {
// Control
populate = () => {
const artistId = this.props.id;
const authorId = this.props.id;
this.props.fetchAlbums({ artistId });
this.props.fetchTrackFiles({ artistId });
this.props.fetchQueueDetails({ artistId });
this.props.fetchAlbums({ authorId });
this.props.fetchSeries({ authorId });
this.props.fetchTrackFiles({ authorId });
this.props.fetchQueueDetails({ authorId });
}
unpopulate = () => {
this.props.cancelFetchReleases();
this.props.clearAlbums();
this.props.clearSeries();
this.props.clearTrackFiles();
this.props.clearQueueDetails();
this.props.clearReleases();
}
//
@ -225,7 +265,7 @@ class ArtistDetailsConnector extends Component {
onMonitorTogglePress = (monitored) => {
this.props.toggleArtistMonitored({
artistId: this.props.id,
authorId: this.props.id,
monitored
});
}
@ -233,14 +273,14 @@ class ArtistDetailsConnector extends Component {
onRefreshPress = () => {
this.props.executeCommand({
name: commandNames.REFRESH_ARTIST,
artistId: this.props.id
authorId: this.props.id
});
}
onSearchPress = () => {
this.props.executeCommand({
name: commandNames.ARTIST_SEARCH,
artistId: this.props.id
authorId: this.props.id
});
}
@ -261,7 +301,7 @@ class ArtistDetailsConnector extends Component {
ArtistDetailsConnector.propTypes = {
id: PropTypes.number.isRequired,
foreignArtistId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
isArtistRefreshing: PropTypes.bool.isRequired,
allArtistRefreshing: PropTypes.bool.isRequired,
isRefreshing: PropTypes.bool.isRequired,
@ -269,11 +309,15 @@ ArtistDetailsConnector.propTypes = {
isRenamingArtist: PropTypes.bool.isRequired,
fetchAlbums: PropTypes.func.isRequired,
clearAlbums: PropTypes.func.isRequired,
fetchSeries: PropTypes.func.isRequired,
clearSeries: PropTypes.func.isRequired,
fetchTrackFiles: PropTypes.func.isRequired,
clearTrackFiles: PropTypes.func.isRequired,
toggleArtistMonitored: PropTypes.func.isRequired,
fetchQueueDetails: PropTypes.func.isRequired,
clearQueueDetails: PropTypes.func.isRequired,
clearReleases: PropTypes.func.isRequired,
cancelFetchReleases: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};

@ -7,26 +7,12 @@ import styles from './ArtistDetailsLinks.css';
function ArtistDetailsLinks(props) {
const {
foreignArtistId,
links
} = props;
return (
<div className={styles.links}>
<Link
className={styles.link}
to={`https://musicbrainz.org/artist/${foreignArtistId}`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
Musicbrainz
</Label>
</Link>
{links.map((link, index) => {
return (
<span key={index}>
@ -56,7 +42,6 @@ function ArtistDetailsLinks(props) {
}
ArtistDetailsLinks.propTypes = {
foreignArtistId: PropTypes.string.isRequired,
links: PropTypes.arrayOf(PropTypes.object).isRequired
};

@ -17,7 +17,7 @@ function createMapStateToProps() {
(state, { match }) => match,
(state) => state.artist,
(match, artist) => {
const foreignArtistId = match.params.foreignArtistId;
const titleSlug = match.params.titleSlug;
const {
isFetching,
isPopulated,
@ -25,13 +25,13 @@ function createMapStateToProps() {
items
} = artist;
const artistIndex = _.findIndex(items, { foreignArtistId });
const artistIndex = _.findIndex(items, { titleSlug });
if (artistIndex > -1) {
return {
isFetching,
isPopulated,
foreignArtistId
titleSlug
};
}
@ -54,7 +54,7 @@ class ArtistDetailsPageConnector extends Component {
// Lifecycle
componentDidUpdate(prevProps) {
if (!this.props.foreignArtistId) {
if (!this.props.titleSlug) {
this.props.push(`${window.Readarr.urlBase}/`);
return;
}
@ -65,7 +65,7 @@ class ArtistDetailsPageConnector extends Component {
render() {
const {
foreignArtistId,
titleSlug,
isFetching,
isPopulated,
error
@ -84,33 +84,33 @@ class ArtistDetailsPageConnector extends Component {
if (!isFetching && !!error) {
return (
<div className={styles.errorMessage}>
{getErrorMessage(error, 'Failed to load artist from API')}
{getErrorMessage(error, 'Failed to load author from API')}
</div>
);
}
if (!foreignArtistId) {
if (!titleSlug) {
return (
<NotFound
message="Sorry, that artist cannot be found."
message="Sorry, that author cannot be found."
/>
);
}
return (
<ArtistDetailsConnector
foreignArtistId={foreignArtistId}
titleSlug={titleSlug}
/>
);
}
}
ArtistDetailsPageConnector.propTypes = {
foreignArtistId: PropTypes.string,
titleSlug: PropTypes.string,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
match: PropTypes.shape({ params: PropTypes.shape({ foreignArtistId: PropTypes.string.isRequired }).isRequired }).isRequired,
match: PropTypes.shape({ params: PropTypes.shape({ titleSlug: PropTypes.string.isRequired }).isRequired }).isRequired,
push: PropTypes.func.isRequired
};

@ -2,14 +2,9 @@ import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getToggledRange from 'Utilities/Table/getToggledRange';
import { icons, sortDirections } from 'Helpers/Props';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import { sortDirections } from 'Helpers/Props';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import AlbumRowConnector from './AlbumRowConnector';
import styles from './ArtistDetailsSeason.css';
@ -22,92 +17,29 @@ class ArtistDetailsSeason extends Component {
super(props, context);
this.state = {
isOrganizeModalOpen: false,
isManageTracksOpen: false,
lastToggledAlbum: null
};
}
componentDidMount() {
this._expandByDefault();
}
componentDidUpdate(prevProps) {
const {
artistId
} = this.props;
if (prevProps.artistId !== artistId) {
this._expandByDefault();
return;
}
}
//
// Control
_expandByDefault() {
const {
name,
onExpandPress,
items,
uiSettings
} = this.props;
const expand = _.some(items, (item) =>
((item.albumType === 'Album') && uiSettings.expandAlbumByDefault) ||
((item.albumType === 'Single') && uiSettings.expandSingleByDefault) ||
((item.albumType === 'EP') && uiSettings.expandEPByDefault) ||
((item.albumType === 'Broadcast') && uiSettings.expandBroadcastByDefault) ||
((item.albumType === 'Other') && uiSettings.expandOtherByDefault));
onExpandPress(name, expand);
}
//
// Listeners
onOrganizePress = () => {
this.setState({ isOrganizeModalOpen: true });
}
onOrganizeModalClose = () => {
this.setState({ isOrganizeModalOpen: false });
}
onManageTracksPress = () => {
this.setState({ isManageTracksOpen: true });
}
onManageTracksModalClose = () => {
this.setState({ isManageTracksOpen: false });
}
onExpandPress = () => {
const {
name,
isExpanded
} = this.props;
this.props.onExpandPress(name, !isExpanded);
}
onMonitorAlbumPress = (albumId, monitored, { shiftKey }) => {
onMonitorAlbumPress = (bookId, monitored, { shiftKey }) => {
const lastToggled = this.state.lastToggledAlbum;
const albumIds = [albumId];
const bookIds = [bookId];
if (shiftKey && lastToggled) {
const { lower, upper } = getToggledRange(this.props.items, albumId, lastToggled);
const { lower, upper } = getToggledRange(this.props.items, bookId, lastToggled);
const items = this.props.items;
for (let i = lower; i < upper; i++) {
albumIds.push(items[i].id);
bookIds.push(items[i].id);
}
}
this.setState({ lastToggledAlbum: albumId });
this.setState({ lastToggledAlbum: bookId });
this.props.onMonitorAlbumPress(_.uniq(albumIds), monitored);
this.props.onMonitorAlbumPress(_.uniq(bookIds), monitored);
}
//
@ -115,134 +47,52 @@ class ArtistDetailsSeason extends Component {
render() {
const {
artistId,
label,
items,
columns,
isExpanded,
sortKey,
sortDirection,
onSortPress,
isSmallScreen,
onTableOptionChange
} = this.props;
const {
isOrganizeModalOpen,
isManageTracksOpen
} = this.state;
return (
<div
className={styles.albumType}
>
<Link
className={styles.expandButton}
onPress={this.onExpandPress}
>
<div className={styles.header}>
<div className={styles.left}>
<div className={styles.albums}>
<Table
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
onTableOptionChange={onTableOptionChange}
>
<TableBody>
{
<div>
<span className={styles.albumTypeLabel}>
{label}
</span>
<span className={styles.albumCount}>
({items.length} Releases)
</span>
</div>
}
</div>
<Icon
className={styles.expandButtonIcon}
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
title={isExpanded ? 'Hide albums' : 'Show albums'}
size={24}
/>
{
!isSmallScreen &&
<span>&nbsp;</span>
}
</div>
</Link>
<div>
{
isExpanded &&
<div className={styles.albums}>
{
items.length ?
<Table
items.map((item) => {
return (
<AlbumRowConnector
key={item.id}
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
onTableOptionChange={onTableOptionChange}
>
<TableBody>
{
items.map((item) => {
return (
<AlbumRowConnector
key={item.id}
columns={columns}
{...item}
onMonitorAlbumPress={this.onMonitorAlbumPress}
/>
);
})
}
</TableBody>
</Table> :
<div className={styles.noAlbums}>
No releases in this group
</div>
}
<div className={styles.collapseButtonContainer}>
<IconButton
iconClassName={styles.collapseButtonIcon}
name={icons.COLLAPSE}
size={20}
title="Hide albums"
onPress={this.onExpandPress}
/>
</div>
</div>
}
{...item}
onMonitorAlbumPress={this.onMonitorAlbumPress}
/>
);
})
}
</TableBody>
</Table>
</div>
<OrganizePreviewModalConnector
isOpen={isOrganizeModalOpen}
artistId={artistId}
onModalClose={this.onOrganizeModalClose}
/>
<TrackFileEditorModal
isOpen={isManageTracksOpen}
artistId={artistId}
onModalClose={this.onManageTracksModalClose}
/>
</div>
);
}
}
ArtistDetailsSeason.propTypes = {
artistId: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isExpanded: PropTypes.bool,
isSmallScreen: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onExpandPress: PropTypes.func.isRequired,
onSortPress: PropTypes.func.isRequired,

@ -23,7 +23,7 @@ function createMapStateToProps() {
createUISettingsSelector(),
(label, albums, artist, commands, dimensions, uiSettings) => {
const albumsInGroup = _.filter(albums.items, { albumType: label });
const albumsInGroup = albums.items;
let sortDir = 'asc';
@ -66,9 +66,9 @@ class ArtistDetailsSeasonConnector extends Component {
this.props.dispatchSetAlbumSort({ sortKey });
}
onMonitorAlbumPress = (albumIds, monitored) => {
onMonitorAlbumPress = (bookIds, monitored) => {
this.props.toggleAlbumsMonitored({
albumIds,
bookIds,
monitored
});
}
@ -89,7 +89,7 @@ class ArtistDetailsSeasonConnector extends Component {
}
ArtistDetailsSeasonConnector.propTypes = {
artistId: PropTypes.number.isRequired,
authorId: PropTypes.number.isRequired,
toggleAlbumsMonitored: PropTypes.func.isRequired,
setAlbumsTableOption: PropTypes.func.isRequired,
dispatchSetAlbumSort: PropTypes.func.isRequired,

@ -1,4 +1,4 @@
.medium {
.albumType {
margin-bottom: 20px;
border: 1px solid $borderColor;
border-radius: 4px;
@ -15,31 +15,36 @@
align-items: center;
width: 100%;
font-size: 24px;
cursor: pointer;
}
.mediumNumber {
margin-right: 10px;
.albumTypeLabel {
margin-right: 5px;
margin-left: 5px;
}
.mediumFormat {
.albumCount {
color: #8895aa;
font-style: italic;
font-size: 18px;
}
.episodeCountTooltip {
display: flex;
}
.expandButton {
composes: link from '~Components/Link/Link.css';
flex-grow: 1;
margin: 0 20px;
width: 100%;
text-align: center;
}
.left {
display: flex;
align-items: center;
flex: 0 1 300px;
flex: 1 1 300px;
}
.left,
@ -57,7 +62,7 @@
composes: menuContent from '~Components/Menu/MenuContent.css';
white-space: nowrap;
font-size: 14px;
font-size: $defaultFontSize;
}
.actionMenuIcon {
@ -70,38 +75,46 @@
width: 30px;
}
.tracks {
.albums {
padding-top: 15px;
border-top: 1px solid $borderColor;
}
.collapseButtonContainer {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 15px;
width: 100%;
border-top: 1px solid $borderColor;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
background-color: #fafafa;
text-align: center;
}
.collapseButtonIcon {
margin-bottom: -4px;
}
.expandButtonIcon {
composes: actionButton;
position: absolute;
top: 50%;
left: 50%;
margin-top: -12px;
margin-left: -15px;
margin-right: 15px;
/* position: absolute; */
/* top: 50%; */
/* left: 90%; */
/* margin-top: -12px; */
/* margin-left: -15px; */
}
.noTracks {
.noAlbums {
margin-bottom: 15px;
text-align: center;
}
@media only screen and (max-width: $breakpointSmall) {
.medium {
.albumType {
border-right: 0;
border-left: 0;
border-radius: 0;

@ -0,0 +1,205 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getToggledRange from 'Utilities/Table/getToggledRange';
import { icons, sortDirections } from 'Helpers/Props';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import AlbumRowConnector from './AlbumRowConnector';
import styles from './AuthorDetailsSeries.css';
class AuthorDetailsSeries extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isOrganizeModalOpen: false,
isManageTracksOpen: false,
lastToggledAlbum: null
};
}
componentDidMount() {
this._expandByDefault();
}
componentDidUpdate(prevProps) {
const {
authorId
} = this.props;
if (prevProps.authorId !== authorId) {
this._expandByDefault();
return;
}
}
//
// Control
_expandByDefault() {
const {
id,
onExpandPress
} = this.props;
onExpandPress(id, true);
}
//
// Listeners
onExpandPress = () => {
const {
id,
isExpanded
} = this.props;
this.props.onExpandPress(id, !isExpanded);
}
onMonitorAlbumPress = (albumId, monitored, { shiftKey }) => {
const lastToggled = this.state.lastToggledAlbum;
const albumIds = [albumId];
if (shiftKey && lastToggled) {
const { lower, upper } = getToggledRange(this.props.items, albumId, lastToggled);
const items = this.props.items;
for (let i = lower; i < upper; i++) {
albumIds.push(items[i].id);
}
}
this.setState({ lastToggledAlbum: albumId });
this.props.onMonitorAlbumPress(_.uniq(albumIds), monitored);
}
//
// Render
render() {
const {
label,
items,
positionMap,
columns,
isExpanded,
sortKey,
sortDirection,
onSortPress,
isSmallScreen,
onTableOptionChange
} = this.props;
return (
<div
className={styles.albumType}
>
<Link
className={styles.expandButton}
onPress={this.onExpandPress}
>
<div className={styles.header}>
<div className={styles.left}>
{
<div>
<span className={styles.albumTypeLabel}>
{label}
</span>
<span className={styles.albumCount}>
({items.length} Books)
</span>
</div>
}
</div>
<Icon
className={styles.expandButtonIcon}
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
title={isExpanded ? 'Hide books' : 'Show books'}
size={24}
/>
{
!isSmallScreen &&
<span>&nbsp;</span>
}
</div>
</Link>
<div>
{
isExpanded &&
<div className={styles.albums}>
<Table
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
onTableOptionChange={onTableOptionChange}
>
<TableBody>
{
items.map((item) => {
return (
<AlbumRowConnector
key={item.id}
columns={columns}
{...item}
position={positionMap[item.id]}
onMonitorAlbumPress={this.onMonitorAlbumPress}
/>
);
})
}
</TableBody>
</Table>
<div className={styles.collapseButtonContainer}>
<IconButton
iconClassName={styles.collapseButtonIcon}
name={icons.COLLAPSE}
size={20}
title="Hide books"
onPress={this.onExpandPress}
/>
</div>
</div>
}
</div>
</div>
);
}
}
AuthorDetailsSeries.propTypes = {
id: PropTypes.number.isRequired,
authorId: PropTypes.number.isRequired,
label: PropTypes.string.isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
items: PropTypes.arrayOf(PropTypes.object).isRequired,
positionMap: PropTypes.object.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isExpanded: PropTypes.bool,
isSmallScreen: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onExpandPress: PropTypes.func.isRequired,
onSortPress: PropTypes.func.isRequired,
onMonitorAlbumPress: PropTypes.func.isRequired,
uiSettings: PropTypes.object.isRequired
};
export default AuthorDetailsSeries;

@ -0,0 +1,121 @@
/* eslint max-params: 0 */
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
// import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { toggleAlbumsMonitored, setAlbumsTableOption } from 'Store/Actions/albumActions';
import { setSeriesSort } from 'Store/Actions/seriesActions';
import { executeCommand } from 'Store/Actions/commandActions';
import AuthorDetailsSeries from './AuthorDetailsSeries';
function createMapStateToProps() {
return createSelector(
(state, { seriesId }) => seriesId,
(state) => state.albums,
createArtistSelector(),
(state) => state.series,
createCommandsSelector(),
createDimensionsSelector(),
createUISettingsSelector(),
(seriesId, books, author, series, commands, dimensions, uiSettings) => {
const currentSeries = _.find(series.items, { id: seriesId });
const bookIds = currentSeries.links.map((x) => x.bookId);
const positionMap = currentSeries.links.reduce((acc, curr) => {
acc[curr.bookId] = curr.position;
return acc;
}, {});
const booksInSeries = _.filter(books.items, (book) => bookIds.includes(book.id));
let sortDir = 'asc';
if (series.sortDirection === 'descending') {
sortDir = 'desc';
}
let sortedBooks = [];
if (series.sortKey === 'position') {
sortedBooks = booksInSeries.sort((a, b) => {
const apos = positionMap[a.id] || '';
const bpos = positionMap[b.id] || '';
return apos.localeCompare(bpos, undefined, { numeric: true, sensivity: 'base' });
});
} else {
sortedBooks = _.orderBy(booksInSeries, series.sortKey, sortDir);
}
return {
id: currentSeries.id,
label: currentSeries.title,
items: sortedBooks,
positionMap,
columns: series.columns,
sortKey: series.sortKey,
sortDirection: series.sortDirection,
artistMonitored: author.monitored,
isSmallScreen: dimensions.isSmallScreen,
uiSettings
};
}
);
}
const mapDispatchToProps = {
toggleAlbumsMonitored,
setAlbumsTableOption,
dispatchSetSeriesSort: setSeriesSort,
executeCommand
};
class ArtistDetailsSeasonConnector extends Component {
//
// Listeners
onTableOptionChange = (payload) => {
this.props.setAlbumsTableOption(payload);
}
onSortPress = (sortKey) => {
this.props.dispatchSetSeriesSort({ sortKey });
}
onMonitorAlbumPress = (bookIds, monitored) => {
this.props.toggleAlbumsMonitored({
bookIds,
monitored
});
}
//
// Render
render() {
return (
<AuthorDetailsSeries
{...this.props}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onMonitorAlbumPress={this.onMonitorAlbumPress}
/>
);
}
}
ArtistDetailsSeasonConnector.propTypes = {
authorId: PropTypes.number.isRequired,
toggleAlbumsMonitored: PropTypes.func.isRequired,
setAlbumsTableOption: PropTypes.func.isRequired,
dispatchSetSeriesSort: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistDetailsSeasonConnector);

@ -72,7 +72,6 @@ class EditArtistModalContent extends Component {
const {
monitored,
albumFolder,
qualityProfileId,
metadataProfileId,
path,
@ -99,18 +98,6 @@ class EditArtistModalContent extends Component {
/>
</FormGroup>
<FormGroup>
<FormLabel>Use Album Folder</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="albumFolder"
helpText="Sort tracks into album folders"
{...albumFolder}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Quality Profile</FormLabel>
@ -213,7 +200,7 @@ class EditArtistModalContent extends Component {
}
EditArtistModalContent.propTypes = {
artistId: PropTypes.number.isRequired,
authorId: PropTypes.number.isRequired,
artistName: PropTypes.string.isRequired,
item: PropTypes.object.isRequired,
isSaving: PropTypes.bool.isRequired,

@ -87,7 +87,7 @@ class EditArtistModalContentConnector extends Component {
onSavePress = (moveFiles) => {
this.props.dispatchSaveArtist({
id: this.props.artistId,
id: this.props.authorId,
moveFiles
});
}
@ -108,7 +108,7 @@ class EditArtistModalContentConnector extends Component {
}
EditArtistModalContentConnector.propTypes = {
artistId: PropTypes.number,
authorId: PropTypes.number,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
dispatchSetArtistValue: PropTypes.func.isRequired,

@ -45,12 +45,6 @@ function getColumns(showMetadataProfile) {
isSortable: true,
isVisible: showMetadataProfile
},
{
name: 'albumFolder',
label: 'Album Folder',
isSortable: true,
isVisible: true
},
{
name: 'path',
label: 'Path',
@ -122,7 +116,7 @@ class ArtistEditor extends Component {
onSaveSelected = (changes) => {
this.props.onSaveSelected({
artistIds: this.getSelectedIds(),
authorIds: this.getSelectedIds(),
...changes
});
}
@ -184,7 +178,7 @@ class ArtistEditor extends Component {
columns
} = this.state;
const selectedArtistIds = this.getSelectedIds();
const selectedAuthorIds = this.getSelectedIds();
return (
<PageContent title="Artist Editor">
@ -252,8 +246,8 @@ class ArtistEditor extends Component {
</PageContentBodyConnector>
<ArtistEditorFooter
artistIds={selectedArtistIds}
selectedCount={selectedArtistIds.length}
authorIds={selectedAuthorIds}
selectedCount={selectedAuthorIds.length}
isSaving={isSaving}
saveError={saveError}
isDeleting={isDeleting}
@ -268,13 +262,13 @@ class ArtistEditor extends Component {
<OrganizeArtistModal
isOpen={this.state.isOrganizingArtistModalOpen}
artistIds={selectedArtistIds}
authorIds={selectedAuthorIds}
onModalClose={this.onOrganizeArtistModalClose}
/>
<RetagArtistModal
isOpen={this.state.isRetaggingArtistModalOpen}
artistIds={selectedArtistIds}
authorIds={selectedAuthorIds}
onModalClose={this.onRetagArtistModalClose}
/>

@ -137,7 +137,7 @@ class ArtistEditorFooter extends Component {
render() {
const {
artistIds,
authorIds,
selectedCount,
isSaving,
isDeleting,
@ -309,14 +309,14 @@ class ArtistEditorFooter extends Component {
<TagsModal
isOpen={isTagsModalOpen}
artistIds={artistIds}
authorIds={authorIds}
onApplyTagsPress={this.onApplyTagsPress}
onModalClose={this.onTagsModalClose}
/>
<DeleteArtistModal
isOpen={isDeleteArtistModalOpen}
artistIds={artistIds}
authorIds={authorIds}
onModalClose={this.onDeleteArtistModalClose}
/>
@ -333,7 +333,7 @@ class ArtistEditorFooter extends Component {
}
ArtistEditorFooter.propTypes = {
artistIds: PropTypes.arrayOf(PropTypes.number).isRequired,
authorIds: PropTypes.arrayOf(PropTypes.number).isRequired,
selectedCount: PropTypes.number.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,

@ -2,7 +2,6 @@ import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TagListConnector from 'Components/TagListConnector';
import CheckInput from 'Components/Form/CheckInput';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
@ -27,13 +26,12 @@ class ArtistEditorRow extends Component {
const {
id,
status,
foreignArtistId,
titleSlug,
artistName,
artistType,
monitored,
metadataProfile,
qualityProfile,
albumFolder,
path,
tags,
columns,
@ -57,7 +55,7 @@ class ArtistEditorRow extends Component {
<TableRowCell className={styles.title}>
<ArtistNameLink
foreignArtistId={foreignArtistId}
titleSlug={titleSlug}
artistName={artistName}
/>
</TableRowCell>
@ -73,15 +71,6 @@ class ArtistEditorRow extends Component {
</TableRowCell>
}
<TableRowCell className={styles.albumFolder}>
<CheckInput
name="albumFolder"
value={albumFolder}
isDisabled={true}
onChange={this.onAlbumFolderChange}
/>
</TableRowCell>
<TableRowCell>
{path}
</TableRowCell>
@ -99,13 +88,12 @@ class ArtistEditorRow extends Component {
ArtistEditorRow.propTypes = {
id: PropTypes.number.isRequired,
status: PropTypes.string.isRequired,
foreignArtistId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired,
artistType: PropTypes.string,
monitored: PropTypes.bool.isRequired,
metadataProfile: PropTypes.object.isRequired,
qualityProfile: PropTypes.object.isRequired,
albumFolder: PropTypes.bool.isRequired,
path: PropTypes.string.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,

@ -10,10 +10,10 @@ import RetagArtistModalContent from './RetagArtistModalContent';
function createMapStateToProps() {
return createSelector(
(state, { artistIds }) => artistIds,
(state, { authorIds }) => authorIds,
createAllArtistSelector(),
(artistIds, allArtists) => {
const artist = _.intersectionWith(allArtists, artistIds, (s, id) => {
(authorIds, allArtists) => {
const artist = _.intersectionWith(allArtists, authorIds, (s, id) => {
return s.id === id;
});
@ -39,7 +39,7 @@ class RetagArtistModalContentConnector extends Component {
onRetagArtistPress = () => {
this.props.executeCommand({
name: commandNames.RETAG_ARTIST,
artistIds: this.props.artistIds
authorIds: this.props.authorIds
});
this.props.onModalClose(true);
@ -59,7 +59,7 @@ class RetagArtistModalContentConnector extends Component {
}
RetagArtistModalContentConnector.propTypes = {
artistIds: PropTypes.arrayOf(PropTypes.number).isRequired,
authorIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onModalClose: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};

@ -7,10 +7,10 @@ import DeleteArtistModalContent from './DeleteArtistModalContent';
function createMapStateToProps() {
return createSelector(
(state, { artistIds }) => artistIds,
(state, { authorIds }) => authorIds,
createAllArtistSelector(),
(artistIds, allArtists) => {
const selectedArtist = _.intersectionWith(allArtists, artistIds, (s, id) => {
(authorIds, allArtists) => {
const selectedArtist = _.intersectionWith(allArtists, authorIds, (s, id) => {
return s.id === id;
});
@ -33,7 +33,7 @@ function createMapDispatchToProps(dispatch, props) {
return {
onDeleteSelectedPress(deleteFiles) {
dispatch(bulkDeleteArtist({
artistIds: props.artistIds,
authorIds: props.authorIds,
deleteFiles
}));

@ -10,10 +10,10 @@ import OrganizeArtistModalContent from './OrganizeArtistModalContent';
function createMapStateToProps() {
return createSelector(
(state, { artistIds }) => artistIds,
(state, { authorIds }) => authorIds,
createAllArtistSelector(),
(artistIds, allArtists) => {
const artist = _.intersectionWith(allArtists, artistIds, (s, id) => {
(authorIds, allArtists) => {
const artist = _.intersectionWith(allArtists, authorIds, (s, id) => {
return s.id === id;
});
@ -39,7 +39,7 @@ class OrganizeArtistModalContentConnector extends Component {
onOrganizeArtistPress = () => {
this.props.executeCommand({
name: commandNames.RENAME_ARTIST,
artistIds: this.props.artistIds
authorIds: this.props.authorIds
});
this.props.onModalClose(true);
@ -59,7 +59,7 @@ class OrganizeArtistModalContentConnector extends Component {
}
OrganizeArtistModalContentConnector.propTypes = {
artistIds: PropTypes.arrayOf(PropTypes.number).isRequired,
authorIds: PropTypes.arrayOf(PropTypes.number).isRequired,
onModalClose: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};

@ -7,11 +7,11 @@ import TagsModalContent from './TagsModalContent';
function createMapStateToProps() {
return createSelector(
(state, { artistIds }) => artistIds,
(state, { authorIds }) => authorIds,
createAllArtistSelector(),
createTagsSelector(),
(artistIds, allArtists, tagList) => {
const artist = _.intersectionWith(allArtists, artistIds, (s, id) => {
(authorIds, allArtists, tagList) => {
const artist = _.intersectionWith(allArtists, authorIds, (s, id) => {
return s.id === id;
});

@ -3,7 +3,6 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchArtistHistory, clearArtistHistory, artistHistoryMarkAsFailed } from 'Store/Actions/artistHistoryActions';
import ArtistHistoryModalContent from './ArtistHistoryModalContent';
function createMapStateToProps() {
return createSelector(
@ -20,20 +19,20 @@ const mapDispatchToProps = {
artistHistoryMarkAsFailed
};
class ArtistHistoryModalContentConnector extends Component {
class ArtistHistoryContentConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
artistId,
albumId
authorId,
bookId
} = this.props;
this.props.fetchArtistHistory({
artistId,
albumId
authorId,
bookId
});
}
@ -46,14 +45,14 @@ class ArtistHistoryModalContentConnector extends Component {
onMarkAsFailedPress = (historyId) => {
const {
artistId,
albumId
authorId,
bookId
} = this.props;
this.props.artistHistoryMarkAsFailed({
historyId,
artistId,
albumId
authorId,
bookId
});
}
@ -61,21 +60,27 @@ class ArtistHistoryModalContentConnector extends Component {
// Render
render() {
const {
component: ViewComponent,
...otherProps
} = this.props;
return (
<ArtistHistoryModalContent
{...this.props}
<ViewComponent
{...otherProps}
onMarkAsFailedPress={this.onMarkAsFailedPress}
/>
);
}
}
ArtistHistoryModalContentConnector.propTypes = {
artistId: PropTypes.number.isRequired,
albumId: PropTypes.number,
ArtistHistoryContentConnector.propTypes = {
component: PropTypes.elementType.isRequired,
authorId: PropTypes.number.isRequired,
bookId: PropTypes.number,
fetchArtistHistory: PropTypes.func.isRequired,
clearArtistHistory: PropTypes.func.isRequired,
artistHistoryMarkAsFailed: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistHistoryModalContentConnector);
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistHistoryContentConnector);

@ -1,7 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ArtistHistoryModalContentConnector from './ArtistHistoryModalContentConnector';
import ArtistHistoryContentConnector from './ArtistHistoryContentConnector';
import ArtistHistoryModalContent from './ArtistHistoryModalContent';
function ArtistHistoryModal(props) {
const {
@ -15,7 +16,8 @@ function ArtistHistoryModal(props) {
isOpen={isOpen}
onModalClose={onModalClose}
>
<ArtistHistoryModalContentConnector
<ArtistHistoryContentConnector
component={ArtistHistoryModalContent}
{...otherProps}
onModalClose={onModalClose}
/>

@ -1,51 +1,11 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import ArtistHistoryRowConnector from './ArtistHistoryRowConnector';
const columns = [
{
name: 'eventType',
isVisible: true
},
{
name: 'album',
label: 'Album',
isVisible: true
},
{
name: 'sourceTitle',
label: 'Source Title',
isVisible: true
},
{
name: 'quality',
label: 'Quality',
isVisible: true
},
{
name: 'date',
label: 'Date',
isVisible: true
},
{
name: 'details',
label: 'Details',
isVisible: true
},
{
name: 'actions',
label: 'Actions',
isVisible: true
}
];
import ArtistHistoryTableContent from './ArtistHistoryTableContent';
class ArtistHistoryModalContent extends Component {
@ -54,18 +14,9 @@ class ArtistHistoryModalContent extends Component {
render() {
const {
albumId,
isFetching,
isPopulated,
error,
items,
onMarkAsFailedPress,
onModalClose
} = this.props;
const fullArtist = albumId == null;
const hasItems = !!items.length;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
@ -73,40 +24,9 @@ class ArtistHistoryModalContent extends Component {
</ModalHeader>
<ModalBody>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to load history.</div>
}
{
isPopulated && !hasItems && !error &&
<div>No history.</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<ArtistHistoryRowConnector
key={item.id}
fullArtist={fullArtist}
{...item}
onMarkAsFailedPress={onMarkAsFailedPress}
/>
);
})
}
</TableBody>
</Table>
}
<ArtistHistoryTableContent
{...this.props}
/>
</ModalBody>
<ModalFooter>
@ -120,12 +40,6 @@ class ArtistHistoryModalContent extends Component {
}
ArtistHistoryModalContent.propTypes = {
albumId: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

@ -0,0 +1,21 @@
import React from 'react';
import ArtistHistoryContentConnector from 'Artist/History/ArtistHistoryContentConnector';
import ArtistHistoryTableContent from 'Artist/History/ArtistHistoryTableContent';
function ArtistHistoryTable(props) {
const {
...otherProps
} = props;
return (
<ArtistHistoryContentConnector
component={ArtistHistoryTableContent}
{...otherProps}
/>
);
}
ArtistHistoryTable.propTypes = {
};
export default ArtistHistoryTable;

@ -0,0 +1,113 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import ArtistHistoryRowConnector from './ArtistHistoryRowConnector';
const columns = [
{
name: 'eventType',
isVisible: true
},
{
name: 'album',
label: 'Album',
isVisible: true
},
{
name: 'sourceTitle',
label: 'Source Title',
isVisible: true
},
{
name: 'quality',
label: 'Quality',
isVisible: true
},
{
name: 'date',
label: 'Date',
isVisible: true
},
{
name: 'details',
label: 'Details',
isVisible: true
},
{
name: 'actions',
label: 'Actions',
isVisible: true
}
];
class ArtistHistoryTableContent extends Component {
//
// Render
render() {
const {
bookId,
isFetching,
isPopulated,
error,
items,
onMarkAsFailedPress
} = this.props;
const fullArtist = bookId == null;
const hasItems = !!items.length;
return (
<>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to load history.</div>
}
{
isPopulated && !hasItems && !error &&
<div>No history.</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.map((item) => {
return (
<ArtistHistoryRowConnector
key={item.id}
fullArtist={fullArtist}
{...item}
onMarkAsFailedPress={onMarkAsFailedPress}
/>
);
})
}
</TableBody>
</Table>
}
</>
);
}
}
ArtistHistoryTableContent.propTypes = {
bookId: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired
};
export default ArtistHistoryTableContent;

@ -60,7 +60,7 @@ class ArtistIndexFooter extends PureComponent {
enableColorImpairedMode && 'colorImpaired'
)}
/>
<div>Continuing (All tracks downloaded)</div>
<div>Continuing (All books downloaded)</div>
</div>
<div className={styles.legendItem}>
@ -70,7 +70,7 @@ class ArtistIndexFooter extends PureComponent {
enableColorImpairedMode && 'colorImpaired'
)}
/>
<div>Ended (All tracks downloaded)</div>
<div>Ended (All books downloaded)</div>
</div>
<div className={styles.legendItem}>
@ -80,7 +80,7 @@ class ArtistIndexFooter extends PureComponent {
enableColorImpairedMode && 'colorImpaired'
)}
/>
<div>Missing Tracks (Artist monitored)</div>
<div>Missing Books (Author monitored)</div>
</div>
<div className={styles.legendItem}>
@ -90,14 +90,14 @@ class ArtistIndexFooter extends PureComponent {
enableColorImpairedMode && 'colorImpaired'
)}
/>
<div>Missing Tracks (Artist not monitored)</div>
<div>Missing Books (Author not monitored)</div>
</div>
</div>
<div className={styles.statistics}>
<DescriptionList>
<DescriptionListItem
title="Artist"
title="Authors"
data={count}
/>
@ -126,7 +126,7 @@ class ArtistIndexFooter extends PureComponent {
<DescriptionList>
<DescriptionListItem
title="Tracks"
title="Books"
data={tracks}
/>

@ -58,14 +58,14 @@ function createMapStateToProps() {
const isRefreshingArtist = executingCommands.some((command) => {
return (
command.name === commandNames.REFRESH_ARTIST &&
command.body.artistId === artist.id
command.body.authorId === artist.id
);
});
const isSearchingArtist = executingCommands.some((command) => {
return (
command.name === commandNames.ARTIST_SEARCH &&
command.body.artistId === artist.id
command.body.authorId === artist.id
);
});
@ -96,14 +96,14 @@ class ArtistIndexItemConnector extends Component {
onRefreshArtistPress = () => {
this.props.dispatchExecuteCommand({
name: commandNames.REFRESH_ARTIST,
artistId: this.props.id
authorId: this.props.id
});
}
onSearchPress = () => {
this.props.dispatchExecuteCommand({
name: commandNames.ARTIST_SEARCH,
artistId: this.props.id
authorId: this.props.id
});
}

@ -63,7 +63,7 @@ $hoverScale: 1.05;
left: 10px;
z-index: 3;
border-radius: 4px;
background-color: #216044;
background-color: $themeLightColor;
color: $white;
font-size: $smallFontSize;
opacity: 0;

@ -58,7 +58,7 @@ class ArtistIndexBanner extends Component {
artistName,
monitored,
status,
foreignArtistId,
titleSlug,
nextAiring,
statistics,
images,
@ -93,7 +93,7 @@ class ArtistIndexBanner extends Component {
isDeleteArtistModalOpen
} = this.state;
const link = `/artist/${foreignArtistId}`;
const link = `/author/${titleSlug}`;
const elementStyle = {
width: `${bannerWidth}px`,
@ -216,14 +216,14 @@ class ArtistIndexBanner extends Component {
<EditArtistModalConnector
isOpen={isEditArtistModalOpen}
artistId={id}
authorId={id}
onModalClose={this.onEditArtistModalClose}
onDeleteArtistPress={this.onDeleteArtistPress}
/>
<DeleteArtistModal
isOpen={isDeleteArtistModalOpen}
artistId={id}
authorId={id}
onModalClose={this.onDeleteArtistModalClose}
/>
</div>
@ -237,7 +237,7 @@ ArtistIndexBanner.propTypes = {
artistName: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
foreignArtistId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
nextAiring: PropTypes.string,
statistics: PropTypes.object.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,

@ -228,7 +228,7 @@ class ArtistIndexBanners extends Component {
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
artistId={artist.id}
authorId={artist.id}
qualityProfileId={artist.qualityProfileId}
metadataProfileId={artist.metadataProfileId}
/>

@ -75,7 +75,7 @@ class ArtistIndexOverview extends Component {
overview,
monitored,
status,
foreignArtistId,
titleSlug,
nextAiring,
statistics,
images,
@ -110,7 +110,7 @@ class ArtistIndexOverview extends Component {
isDeleteArtistModalOpen
} = this.state;
const link = `/artist/${foreignArtistId}`;
const link = `/author/${titleSlug}`;
const elementStyle = {
width: `${posterWidth}px`,
@ -228,14 +228,14 @@ class ArtistIndexOverview extends Component {
<EditArtistModalConnector
isOpen={isEditArtistModalOpen}
artistId={id}
authorId={id}
onModalClose={this.onEditArtistModalClose}
onDeleteArtistPress={this.onDeleteArtistPress}
/>
<DeleteArtistModal
isOpen={isDeleteArtistModalOpen}
artistId={id}
authorId={id}
onModalClose={this.onDeleteArtistModalClose}
/>
</div>
@ -249,7 +249,7 @@ ArtistIndexOverview.propTypes = {
overview: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
foreignArtistId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
nextAiring: PropTypes.string,
statistics: PropTypes.object.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,

@ -172,7 +172,7 @@ class ArtistIndexOverviews extends Component {
longDateFormat={longDateFormat}
timeFormat={timeFormat}
isSmallScreen={isSmallScreen}
artistId={artist.id}
authorId={artist.id}
qualityProfileId={artist.qualityProfileId}
metadataProfileId={artist.metadataProfileId}
/>

@ -81,7 +81,7 @@ $hoverScale: 1.05;
left: 10px;
z-index: 3;
border-radius: 4px;
background-color: #216044;
background-color: $themeLightColor;
color: $white;
font-size: $smallFontSize;
opacity: 0;

@ -70,7 +70,7 @@ class ArtistIndexPoster extends Component {
id,
artistName,
monitored,
foreignArtistId,
titleSlug,
status,
nextAiring,
statistics,
@ -107,12 +107,13 @@ class ArtistIndexPoster extends Component {
isDeleteArtistModalOpen
} = this.state;
const link = `/artist/${foreignArtistId}`;
const link = `/author/${titleSlug}`;
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
};
elementStyle['object-fit'] = 'contain';
return (
<div className={styles.container}>
@ -239,14 +240,14 @@ class ArtistIndexPoster extends Component {
<EditArtistModalConnector
isOpen={isEditArtistModalOpen}
artistId={id}
authorId={id}
onModalClose={this.onEditArtistModalClose}
onDeleteArtistPress={this.onDeleteArtistPress}
/>
<DeleteArtistModal
isOpen={isDeleteArtistModalOpen}
artistId={id}
authorId={id}
onModalClose={this.onDeleteArtistModalClose}
/>
</div>
@ -260,7 +261,7 @@ ArtistIndexPoster.propTypes = {
artistName: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
foreignArtistId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
nextAiring: PropTypes.string,
statistics: PropTypes.object.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,

@ -204,8 +204,8 @@ class ArtistIndexPosters extends Component {
showQualityProfile
} = posterOptions;
const artistIdx = rowIndex * columnCount + columnIndex;
const artist = items[artistIdx];
const authorIdx = rowIndex * columnCount + columnIndex;
const artist = items[authorIdx];
if (!artist) {
return null;
@ -229,7 +229,7 @@ class ArtistIndexPosters extends Component {
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
artistId={artist.id}
authorId={artist.id}
qualityProfileId={artist.qualityProfileId}
metadataProfileId={artist.metadataProfileId}
/>

@ -78,14 +78,14 @@ class ArtistIndexActionsCell extends Component {
<EditArtistModalConnector
isOpen={isEditArtistModalOpen}
artistId={id}
authorId={id}
onModalClose={this.onEditArtistModalClose}
onDeleteArtistPress={this.onDeleteArtistPress}
/>
<DeleteArtistModal
isOpen={isDeleteArtistModalOpen}
artistId={id}
authorId={id}
onModalClose={this.onDeleteArtistModalClose}
/>
</VirtualTableRowCell>

@ -81,7 +81,7 @@ class ArtistIndexRow extends Component {
monitored,
status,
artistName,
foreignArtistId,
titleSlug,
artistType,
qualityProfile,
metadataProfile,
@ -157,7 +157,7 @@ class ArtistIndexRow extends Component {
showBanners ?
<Link
className={styles.link}
to={`/artist/${foreignArtistId}`}
to={`/author/${titleSlug}`}
>
<ArtistBanner
className={styles.bannerImage}
@ -177,7 +177,7 @@ class ArtistIndexRow extends Component {
</Link> :
<ArtistNameLink
foreignArtistId={foreignArtistId}
titleSlug={titleSlug}
artistName={artistName}
/>
}
@ -228,7 +228,7 @@ class ArtistIndexRow extends Component {
<AlbumTitleLink
title={nextAlbum.title}
disambiguation={nextAlbum.disambiguation}
foreignAlbumId={nextAlbum.foreignAlbumId}
titleSlug={nextAlbum.titleSlug}
/>
</VirtualTableRowCell>
);
@ -253,7 +253,7 @@ class ArtistIndexRow extends Component {
<AlbumTitleLink
title={lastAlbum.title}
disambiguation={lastAlbum.disambiguation}
foreignAlbumId={lastAlbum.foreignAlbumId}
titleSlug={lastAlbum.titleSlug}
/>
</VirtualTableRowCell>
);
@ -423,14 +423,14 @@ class ArtistIndexRow extends Component {
<EditArtistModalConnector
isOpen={isEditArtistModalOpen}
artistId={id}
authorId={id}
onModalClose={this.onEditArtistModalClose}
onDeleteArtistPress={this.onDeleteArtistPress}
/>
<DeleteArtistModal
isOpen={isDeleteArtistModalOpen}
artistId={id}
authorId={id}
onModalClose={this.onDeleteArtistModalClose}
/>
</>
@ -443,7 +443,7 @@ ArtistIndexRow.propTypes = {
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired,
foreignArtistId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
artistType: PropTypes.string,
qualityProfile: PropTypes.object.isRequired,
metadataProfile: PropTypes.object.isRequired,

@ -62,7 +62,7 @@ class ArtistIndexTable extends Component {
component={ArtistIndexRow}
style={style}
columns={columns}
artistId={artist.id}
authorId={artist.id}
qualityProfileId={artist.qualityProfileId}
metadataProfileId={artist.metadataProfileId}
showBanners={showBanners}

@ -11,7 +11,7 @@ function NoArtist(props) {
return (
<div>
<div className={styles.message}>
All artists are hidden due to the applied filter.
All authors are hidden due to the applied filter.
</div>
</div>
);
@ -20,7 +20,7 @@ function NoArtist(props) {
return (
<div>
<div className={styles.message}>
No artists found, to get started you'll want to add a new artist or album or add an existing library location (Root Folder) and update.
No authors found, to get started you'll want to add a new author or book or add an existing library location (Root Folder) and update.
</div>
<div className={styles.buttonContainer}>
@ -37,7 +37,7 @@ function NoArtist(props) {
to="/add/search"
kind={kinds.PRIMARY}
>
Add New Artist
Add New Author
</Button>
</div>
</div>

@ -1,33 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import ArtistInteractiveSearchModalContent from './ArtistInteractiveSearchModalContent';
function ArtistInteractiveSearchModal(props) {
const {
isOpen,
artistId,
onModalClose
} = props;
return (
<Modal
isOpen={isOpen}
closeOnBackgroundClick={false}
onModalClose={onModalClose}
>
<ArtistInteractiveSearchModalContent
artistId={artistId}
onModalClose={onModalClose}
/>
</Modal>
);
}
ArtistInteractiveSearchModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
artistId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default ArtistInteractiveSearchModal;

@ -1,15 +0,0 @@
import { connect } from 'react-redux';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import ArtistInteractiveSearchModal from './ArtistInteractiveSearchModal';
function createMapDispatchToProps(dispatch, props) {
return {
onModalClose() {
dispatch(cancelFetchReleases());
dispatch(clearReleases());
props.onModalClose();
}
};
}
export default connect(null, createMapDispatchToProps)(ArtistInteractiveSearchModal);

@ -1,45 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
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 InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
function ArtistInteractiveSearchModalContent(props) {
const {
artistId,
onModalClose
} = props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Interactive Search
</ModalHeader>
<ModalBody>
<InteractiveSearchConnector
type="artist"
searchPayload={{
artistId
}}
/>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Close
</Button>
</ModalFooter>
</ModalContent>
);
}
ArtistInteractiveSearchModalContent.propTypes = {
artistId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default ArtistInteractiveSearchModalContent;

@ -20,7 +20,7 @@ function Agenda(props) {
return (
<AgendaEventConnector
key={item.id}
albumId={item.id}
bookId={item.id}
showDate={showDate}
{...item}
/>

@ -41,7 +41,7 @@ class AgendaEvent extends Component {
id,
artist,
title,
foreignAlbumId,
titleSlug,
releaseDate,
monitored,
statistics,
@ -86,7 +86,7 @@ class AgendaEvent extends Component {
</div>
<div className={styles.artistName}>
<Link to={`/artist/${artist.foreignArtistId}`}>
<Link to={`/author/${artist.titleSlug}`}>
{artist.artistName}
</Link>
</div>
@ -94,7 +94,7 @@ class AgendaEvent extends Component {
<div className={styles.albumSeparator}> - </div>
<div className={styles.albumTitle}>
<Link to={`/album/${foreignAlbumId}`}>
<Link to={`/book/${titleSlug}`}>
{title}
</Link>
</div>
@ -123,7 +123,7 @@ AgendaEvent.propTypes = {
id: PropTypes.number.isRequired,
artist: PropTypes.object.isRequired,
title: PropTypes.string.isRequired,
foreignAlbumId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
albumType: PropTypes.string.isRequired,
releaseDate: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save