(cherry picked from commit d022679b7dcbce3cec98e6a1fd0879e3c0d92523) Fixed: Restoring scroll position when going back/forward to artist list (cherry picked from commit 5aad84dba453c42b4b5a9eac43deecf91a98f4f6)pull/4223/head
parent
01e21c09db
commit
f509ca0f72
@ -0,0 +1,21 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
export interface Statistics {
|
||||
trackCount: number;
|
||||
trackFileCount: number;
|
||||
percentOfTracks: number;
|
||||
sizeOnDisk: number;
|
||||
totalTrackCount: number;
|
||||
}
|
||||
|
||||
interface Album extends ModelBase {
|
||||
foreignAlbumId: string;
|
||||
title: string;
|
||||
overview: string;
|
||||
disambiguation?: string;
|
||||
monitored: boolean;
|
||||
releaseDate: string;
|
||||
statistics: Statistics;
|
||||
}
|
||||
|
||||
export default Album;
|
@ -0,0 +1,72 @@
|
||||
import AppSectionState, {
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Artist from 'Artist/Artist';
|
||||
import Column from 'Components/Table/Column';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import { Filter, FilterBuilderProp } from './AppState';
|
||||
|
||||
export interface ArtistIndexAppState {
|
||||
sortKey: string;
|
||||
sortDirection: SortDirection;
|
||||
secondarySortKey: string;
|
||||
secondarySortDirection: SortDirection;
|
||||
view: string;
|
||||
|
||||
posterOptions: {
|
||||
detailedProgressBar: boolean;
|
||||
size: string;
|
||||
showTitle: boolean;
|
||||
showMonitored: boolean;
|
||||
showQualityProfile: boolean;
|
||||
showNextAlbum: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
bannerOptions: {
|
||||
detailedProgressBar: boolean;
|
||||
size: string;
|
||||
showTitle: boolean;
|
||||
showMonitored: boolean;
|
||||
showQualityProfile: boolean;
|
||||
showNextAlbum: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
overviewOptions: {
|
||||
detailedProgressBar: boolean;
|
||||
size: string;
|
||||
showMonitored: boolean;
|
||||
showQualityProfile: boolean;
|
||||
showLastAlbum: boolean;
|
||||
showAdded: boolean;
|
||||
showAlbumCount: boolean;
|
||||
showPath: boolean;
|
||||
showSizeOnDisk: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
tableOptions: {
|
||||
showBanners: boolean;
|
||||
showSearchAction: boolean;
|
||||
};
|
||||
|
||||
selectedFilterKey: string;
|
||||
filterBuilderProps: FilterBuilderProp<Artist>[];
|
||||
filters: Filter[];
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
interface ArtistAppState
|
||||
extends AppSectionState<Artist>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {
|
||||
itemMap: Record<number, number>;
|
||||
|
||||
deleteOptions: {
|
||||
addImportListExclusion: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export default ArtistAppState;
|
@ -0,0 +1,51 @@
|
||||
import Album from 'Album/Album';
|
||||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
export interface Image {
|
||||
coverType: string;
|
||||
url: string;
|
||||
remoteUrl: string;
|
||||
}
|
||||
|
||||
export interface Statistics {
|
||||
albumCount: number;
|
||||
trackCount: number;
|
||||
trackFileCount: number;
|
||||
percentOfTracks: number;
|
||||
sizeOnDisk: number;
|
||||
totalTrackCount: number;
|
||||
}
|
||||
|
||||
export interface Ratings {
|
||||
votes: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface Artist extends ModelBase {
|
||||
added: string;
|
||||
foreignArtistId: string;
|
||||
cleanName: string;
|
||||
ended: boolean;
|
||||
genres: string[];
|
||||
images: Image[];
|
||||
monitored: boolean;
|
||||
overview: string;
|
||||
path: string;
|
||||
lastAlbum?: Album;
|
||||
nextAlbum?: Album;
|
||||
qualityProfileId: number;
|
||||
metadataProfileId: number;
|
||||
ratings: Ratings;
|
||||
rootFolderPath: string;
|
||||
albums: Album[];
|
||||
sortName: string;
|
||||
statistics: Statistics;
|
||||
status: string;
|
||||
tags: number[];
|
||||
artistName: string;
|
||||
artistType?: string;
|
||||
disambiguation?: string;
|
||||
isSaving?: boolean;
|
||||
}
|
||||
|
||||
export default Artist;
|
@ -1,407 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import NoArtist from 'Artist/NoArtist';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageJumpBar from 'Components/Page/PageJumpBar';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ArtistIndexFooterConnector from './ArtistIndexFooterConnector';
|
||||
import ArtistIndexBannersConnector from './Banners/ArtistIndexBannersConnector';
|
||||
import ArtistIndexBannerOptionsModal from './Banners/Options/ArtistIndexBannerOptionsModal';
|
||||
import ArtistIndexFilterMenu from './Menus/ArtistIndexFilterMenu';
|
||||
import ArtistIndexSortMenu from './Menus/ArtistIndexSortMenu';
|
||||
import ArtistIndexViewMenu from './Menus/ArtistIndexViewMenu';
|
||||
import ArtistIndexOverviewsConnector from './Overview/ArtistIndexOverviewsConnector';
|
||||
import ArtistIndexOverviewOptionsModal from './Overview/Options/ArtistIndexOverviewOptionsModal';
|
||||
import ArtistIndexPostersConnector from './Posters/ArtistIndexPostersConnector';
|
||||
import ArtistIndexPosterOptionsModal from './Posters/Options/ArtistIndexPosterOptionsModal';
|
||||
import ArtistIndexTableConnector from './Table/ArtistIndexTableConnector';
|
||||
import ArtistIndexTableOptionsConnector from './Table/ArtistIndexTableOptionsConnector';
|
||||
import styles from './ArtistIndex.css';
|
||||
|
||||
function getViewComponent(view) {
|
||||
if (view === 'posters') {
|
||||
return ArtistIndexPostersConnector;
|
||||
}
|
||||
|
||||
if (view === 'banners') {
|
||||
return ArtistIndexBannersConnector;
|
||||
}
|
||||
|
||||
if (view === 'overview') {
|
||||
return ArtistIndexOverviewsConnector;
|
||||
}
|
||||
|
||||
return ArtistIndexTableConnector;
|
||||
}
|
||||
|
||||
class ArtistIndex extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
scroller: null,
|
||||
jumpBarItems: { order: [] },
|
||||
jumpToCharacter: null,
|
||||
isPosterOptionsModalOpen: false,
|
||||
isBannerOptionsModalOpen: false,
|
||||
isOverviewOptionsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setJumpBarItems();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection
|
||||
} = this.props;
|
||||
|
||||
if (sortKey !== prevProps.sortKey ||
|
||||
sortDirection !== prevProps.sortDirection ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||
) {
|
||||
this.setJumpBarItems();
|
||||
}
|
||||
|
||||
if (this.state.jumpToCharacter != null) {
|
||||
this.setState({ jumpToCharacter: null });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setScrollerRef = (ref) => {
|
||||
this.setState({ scroller: ref });
|
||||
};
|
||||
|
||||
setJumpBarItems() {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection
|
||||
} = this.props;
|
||||
|
||||
// Reset if not sorting by sortName
|
||||
if (sortKey !== 'sortName') {
|
||||
this.setState({ jumpBarItems: { order: [] } });
|
||||
return;
|
||||
}
|
||||
|
||||
const characters = _.reduce(items, (acc, item) => {
|
||||
let char = item.sortName.charAt(0);
|
||||
|
||||
if (!isNaN(char)) {
|
||||
char = '#';
|
||||
}
|
||||
|
||||
if (char in acc) {
|
||||
acc[char] = acc[char] + 1;
|
||||
} else {
|
||||
acc[char] = 1;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const order = Object.keys(characters).sort();
|
||||
|
||||
// Reverse if sorting descending
|
||||
if (sortDirection === sortDirections.DESCENDING) {
|
||||
order.reverse();
|
||||
}
|
||||
|
||||
const jumpBarItems = {
|
||||
characters,
|
||||
order
|
||||
};
|
||||
|
||||
this.setState({ jumpBarItems });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPosterOptionsPress = () => {
|
||||
this.setState({ isPosterOptionsModalOpen: true });
|
||||
};
|
||||
|
||||
onPosterOptionsModalClose = () => {
|
||||
this.setState({ isPosterOptionsModalOpen: false });
|
||||
};
|
||||
|
||||
onBannerOptionsPress = () => {
|
||||
this.setState({ isBannerOptionsModalOpen: true });
|
||||
};
|
||||
|
||||
onBannerOptionsModalClose = () => {
|
||||
this.setState({ isBannerOptionsModalOpen: false });
|
||||
};
|
||||
|
||||
onOverviewOptionsPress = () => {
|
||||
this.setState({ isOverviewOptionsModalOpen: true });
|
||||
};
|
||||
|
||||
onOverviewOptionsModalClose = () => {
|
||||
this.setState({ isOverviewOptionsModalOpen: false });
|
||||
};
|
||||
|
||||
onJumpBarItemPress = (jumpToCharacter) => {
|
||||
this.setState({ jumpToCharacter });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
totalItems,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
view,
|
||||
isRefreshingArtist,
|
||||
isRssSyncExecuting,
|
||||
onScroll,
|
||||
onSortSelect,
|
||||
onFilterSelect,
|
||||
onViewSelect,
|
||||
onRefreshArtistPress,
|
||||
onRssSyncPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
scroller,
|
||||
jumpBarItems,
|
||||
jumpToCharacter,
|
||||
isPosterOptionsModalOpen,
|
||||
isBannerOptionsModalOpen,
|
||||
isOverviewOptionsModalOpen
|
||||
} = this.state;
|
||||
|
||||
const ViewComponent = getViewComponent(view);
|
||||
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
||||
const hasNoArtist = !totalItems;
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('UpdateAll')}
|
||||
iconName={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
isSpinning={isRefreshingArtist}
|
||||
onPress={onRefreshArtistPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('RSSSync')}
|
||||
iconName={icons.RSS}
|
||||
isSpinning={isRssSyncExecuting}
|
||||
isDisabled={hasNoArtist}
|
||||
onPress={onRssSyncPress}
|
||||
/>
|
||||
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection
|
||||
alignContent={align.RIGHT}
|
||||
collapseButtons={false}
|
||||
>
|
||||
{
|
||||
view === 'table' ?
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
optionsComponent={ArtistIndexTableOptionsConnector}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
view === 'posters' ?
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.POSTER}
|
||||
isDisabled={hasNoArtist}
|
||||
onPress={this.onPosterOptionsPress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
view === 'banners' ?
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.POSTER}
|
||||
isDisabled={hasNoArtist}
|
||||
onPress={this.onBannerOptionsPress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
view === 'overview' ?
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.OVERVIEW}
|
||||
isDisabled={hasNoArtist}
|
||||
onPress={this.onOverviewOptionsPress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<ArtistIndexViewMenu
|
||||
view={view}
|
||||
isDisabled={hasNoArtist}
|
||||
onViewSelect={onViewSelect}
|
||||
/>
|
||||
|
||||
<ArtistIndexSortMenu
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
isDisabled={hasNoArtist}
|
||||
onSortSelect={onSortSelect}
|
||||
/>
|
||||
|
||||
<ArtistIndexFilterMenu
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
isDisabled={hasNoArtist}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<div className={styles.pageContentBodyWrapper}>
|
||||
<PageContentBody
|
||||
registerScroller={this.setScrollerRef}
|
||||
className={styles.contentBody}
|
||||
innerClassName={styles[`${view}InnerContentBody`]}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div className={styles.errorMessage}>
|
||||
{getErrorMessage(error, 'Failed to load artist from API')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isLoaded &&
|
||||
<div className={styles.contentBodyContainer}>
|
||||
<ViewComponent
|
||||
scroller={scroller}
|
||||
items={items}
|
||||
filters={filters}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
jumpToCharacter={jumpToCharacter}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
<ArtistIndexFooterConnector />
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoArtist totalItems={totalItems} />
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
isLoaded && !!jumpBarItems.order.length &&
|
||||
<PageJumpBar
|
||||
items={jumpBarItems}
|
||||
onItemPress={this.onJumpBarItemPress}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ArtistIndexPosterOptionsModal
|
||||
isOpen={isPosterOptionsModalOpen}
|
||||
onModalClose={this.onPosterOptionsModalClose}
|
||||
/>
|
||||
|
||||
<ArtistIndexBannerOptionsModal
|
||||
isOpen={isBannerOptionsModalOpen}
|
||||
onModalClose={this.onBannerOptionsModalClose}
|
||||
|
||||
/>
|
||||
|
||||
<ArtistIndexOverviewOptionsModal
|
||||
isOpen={isOverviewOptionsModalOpen}
|
||||
onModalClose={this.onOverviewOptionsModalClose}
|
||||
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndex.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
totalItems: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
view: PropTypes.string.isRequired,
|
||||
isRefreshingArtist: PropTypes.bool.isRequired,
|
||||
isRssSyncExecuting: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onSortSelect: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onViewSelect: PropTypes.func.isRequired,
|
||||
onRefreshArtistPress: PropTypes.func.isRequired,
|
||||
onRssSyncPress: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndex;
|
@ -0,0 +1,333 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import NoArtist from 'Artist/NoArtist';
|
||||
import { REFRESH_ARTIST, RSS_SYNC } from 'Commands/commandNames';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageJumpBar from 'Components/Page/PageJumpBar';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import withScrollPosition from 'Components/withScrollPosition';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import {
|
||||
setArtistFilter,
|
||||
setArtistSort,
|
||||
setArtistTableOption,
|
||||
setArtistView,
|
||||
} from 'Store/Actions/artistIndexActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
import createArtistClientSideCollectionItemsSelector from 'Store/Selectors/createArtistClientSideCollectionItemsSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ArtistIndexFooter from './ArtistIndexFooter';
|
||||
import ArtistIndexBanners from './Banners/ArtistIndexBanners';
|
||||
import ArtistIndexBannerOptionsModal from './Banners/Options/ArtistIndexBannerOptionsModal';
|
||||
import ArtistIndexFilterMenu from './Menus/ArtistIndexFilterMenu';
|
||||
import ArtistIndexSortMenu from './Menus/ArtistIndexSortMenu';
|
||||
import ArtistIndexViewMenu from './Menus/ArtistIndexViewMenu';
|
||||
import ArtistIndexOverviews from './Overview/ArtistIndexOverviews';
|
||||
import ArtistIndexOverviewOptionsModal from './Overview/Options/ArtistIndexOverviewOptionsModal';
|
||||
import ArtistIndexPosters from './Posters/ArtistIndexPosters';
|
||||
import ArtistIndexPosterOptionsModal from './Posters/Options/ArtistIndexPosterOptionsModal';
|
||||
import ArtistIndexTable from './Table/ArtistIndexTable';
|
||||
import ArtistIndexTableOptions from './Table/ArtistIndexTableOptions';
|
||||
import styles from './ArtistIndex.css';
|
||||
|
||||
function getViewComponent(view) {
|
||||
if (view === 'posters') {
|
||||
return ArtistIndexPosters;
|
||||
}
|
||||
|
||||
if (view === 'banners') {
|
||||
return ArtistIndexBanners;
|
||||
}
|
||||
|
||||
if (view === 'overview') {
|
||||
return ArtistIndexOverviews;
|
||||
}
|
||||
|
||||
return ArtistIndexTable;
|
||||
}
|
||||
|
||||
interface ArtistIndexProps {
|
||||
initialScrollTop?: number;
|
||||
}
|
||||
|
||||
const ArtistIndex = withScrollPosition((props: ArtistIndexProps) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
totalItems,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
view,
|
||||
} = useSelector(createArtistClientSideCollectionItemsSelector('artistIndex'));
|
||||
|
||||
const isRefreshingArtist = useSelector(
|
||||
createCommandExecutingSelector(REFRESH_ARTIST)
|
||||
);
|
||||
const isRssSyncExecuting = useSelector(
|
||||
createCommandExecutingSelector(RSS_SYNC)
|
||||
);
|
||||
const { isSmallScreen } = useSelector(createDimensionsSelector());
|
||||
const dispatch = useDispatch();
|
||||
const scrollerRef = useRef<HTMLDivElement>();
|
||||
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
|
||||
const [jumpToCharacter, setJumpToCharacter] = useState<string | null>(null);
|
||||
|
||||
const onRefreshArtistPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: REFRESH_ARTIST,
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
const onRssSyncPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: RSS_SYNC,
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
const onTableOptionChange = useCallback(
|
||||
(payload) => {
|
||||
dispatch(setArtistTableOption(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onViewSelect = useCallback(
|
||||
(value) => {
|
||||
dispatch(setArtistView({ view: value }));
|
||||
|
||||
if (scrollerRef.current) {
|
||||
scrollerRef.current.scrollTo(0, 0);
|
||||
}
|
||||
},
|
||||
[scrollerRef, dispatch]
|
||||
);
|
||||
|
||||
const onSortSelect = useCallback(
|
||||
(value) => {
|
||||
dispatch(setArtistSort({ sortKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onFilterSelect = useCallback(
|
||||
(value) => {
|
||||
dispatch(setArtistFilter({ selectedFilterKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onOptionsPress = useCallback(() => {
|
||||
setIsOptionsModalOpen(true);
|
||||
}, [setIsOptionsModalOpen]);
|
||||
|
||||
const onOptionsModalClose = useCallback(() => {
|
||||
setIsOptionsModalOpen(false);
|
||||
}, [setIsOptionsModalOpen]);
|
||||
|
||||
const onJumpBarItemPress = useCallback(
|
||||
(character) => {
|
||||
setJumpToCharacter(character);
|
||||
},
|
||||
[setJumpToCharacter]
|
||||
);
|
||||
|
||||
const onScroll = useCallback(
|
||||
({ scrollTop }) => {
|
||||
setJumpToCharacter(null);
|
||||
scrollPositions.artistIndex = scrollTop;
|
||||
},
|
||||
[setJumpToCharacter]
|
||||
);
|
||||
|
||||
const jumpBarItems = useMemo(() => {
|
||||
// Reset if not sorting by sortName
|
||||
if (sortKey !== 'sortName') {
|
||||
return {
|
||||
order: [],
|
||||
};
|
||||
}
|
||||
|
||||
const characters = items.reduce((acc, item) => {
|
||||
let char = item.sortName.charAt(0);
|
||||
|
||||
if (!isNaN(char)) {
|
||||
char = '#';
|
||||
}
|
||||
|
||||
if (char in acc) {
|
||||
acc[char] = acc[char] + 1;
|
||||
} else {
|
||||
acc[char] = 1;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const order = Object.keys(characters).sort();
|
||||
|
||||
// Reverse if sorting descending
|
||||
if (sortDirection === SortDirection.Descending) {
|
||||
order.reverse();
|
||||
}
|
||||
|
||||
return {
|
||||
characters,
|
||||
order,
|
||||
};
|
||||
}, [items, sortKey, sortDirection]);
|
||||
const ViewComponent = useMemo(() => getViewComponent(view), [view]);
|
||||
|
||||
const isLoaded = !!(!error && isPopulated && items.length);
|
||||
const hasNoArtist = !totalItems;
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('UpdateAll')}
|
||||
iconName={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
isSpinning={isRefreshingArtist}
|
||||
isDisabled={hasNoArtist}
|
||||
onPress={onRefreshArtistPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('RSSSync')}
|
||||
iconName={icons.RSS}
|
||||
isSpinning={isRssSyncExecuting}
|
||||
isDisabled={hasNoArtist}
|
||||
onPress={onRssSyncPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT} collapseButtons={false}>
|
||||
{view === 'table' ? (
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
optionsComponent={ArtistIndexTableOptions}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
) : (
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={view === 'posters' ? icons.POSTER : icons.OVERVIEW}
|
||||
isDisabled={hasNoArtist}
|
||||
onPress={onOptionsPress}
|
||||
/>
|
||||
)}
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<ArtistIndexViewMenu
|
||||
view={view}
|
||||
isDisabled={hasNoArtist}
|
||||
onViewSelect={onViewSelect}
|
||||
/>
|
||||
|
||||
<ArtistIndexSortMenu
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
isDisabled={hasNoArtist}
|
||||
onSortSelect={onSortSelect}
|
||||
/>
|
||||
|
||||
<ArtistIndexFilterMenu
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
isDisabled={hasNoArtist}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
<div className={styles.pageContentBodyWrapper}>
|
||||
<PageContentBody
|
||||
ref={scrollerRef}
|
||||
className={styles.contentBody}
|
||||
innerClassName={styles[`${view}InnerContentBody`]}
|
||||
initialScrollTop={props.initialScrollTop}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && !!error ? (
|
||||
<div className={styles.errorMessage}>
|
||||
{getErrorMessage(error, 'Failed to load artist from API')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isLoaded ? (
|
||||
<div className={styles.contentBodyContainer}>
|
||||
<ViewComponent
|
||||
scrollerRef={scrollerRef}
|
||||
items={items}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
jumpToCharacter={jumpToCharacter}
|
||||
isSmallScreen={isSmallScreen}
|
||||
/>
|
||||
|
||||
<ArtistIndexFooter />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!error && isPopulated && !items.length ? (
|
||||
<NoArtist totalItems={totalItems} />
|
||||
) : null}
|
||||
</PageContentBody>
|
||||
|
||||
{isLoaded && !!jumpBarItems.order.length ? (
|
||||
<PageJumpBar items={jumpBarItems} onItemPress={onJumpBarItemPress} />
|
||||
) : null}
|
||||
</div>
|
||||
{view === 'posters' ? (
|
||||
<ArtistIndexPosterOptionsModal
|
||||
isOpen={isOptionsModalOpen}
|
||||
onModalClose={onOptionsModalClose}
|
||||
/>
|
||||
) : null}
|
||||
{view === 'banners' ? (
|
||||
<ArtistIndexBannerOptionsModal
|
||||
isOpen={isOptionsModalOpen}
|
||||
onModalClose={onOptionsModalClose}
|
||||
/>
|
||||
) : null}
|
||||
{view === 'overview' ? (
|
||||
<ArtistIndexOverviewOptionsModal
|
||||
isOpen={isOptionsModalOpen}
|
||||
onModalClose={onOptionsModalClose}
|
||||
/>
|
||||
) : null}
|
||||
</PageContent>
|
||||
);
|
||||
}, 'artistIndex');
|
||||
|
||||
export default ArtistIndex;
|
@ -1,106 +0,0 @@
|
||||
/* eslint max-params: 0 */
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withScrollPosition from 'Components/withScrollPosition';
|
||||
import { setArtistFilter, setArtistSort, setArtistTableOption, setArtistView } from 'Store/Actions/artistIndexActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
import createArtistClientSideCollectionItemsSelector from 'Store/Selectors/createArtistClientSideCollectionItemsSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import ArtistIndex from './ArtistIndex';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createArtistClientSideCollectionItemsSelector('artistIndex'),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_ARTIST),
|
||||
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
||||
createDimensionsSelector(),
|
||||
(
|
||||
artist,
|
||||
isRefreshingArtist,
|
||||
isRssSyncExecuting,
|
||||
dimensionsState
|
||||
) => {
|
||||
return {
|
||||
...artist,
|
||||
isRefreshingArtist,
|
||||
isRssSyncExecuting,
|
||||
isSmallScreen: dimensionsState.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onTableOptionChange(payload) {
|
||||
dispatch(setArtistTableOption(payload));
|
||||
},
|
||||
|
||||
onSortSelect(sortKey) {
|
||||
dispatch(setArtistSort({ sortKey }));
|
||||
},
|
||||
|
||||
onFilterSelect(selectedFilterKey) {
|
||||
dispatch(setArtistFilter({ selectedFilterKey }));
|
||||
},
|
||||
|
||||
dispatchSetArtistView(view) {
|
||||
dispatch(setArtistView({ view }));
|
||||
},
|
||||
|
||||
onRefreshArtistPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.REFRESH_ARTIST
|
||||
}));
|
||||
},
|
||||
|
||||
onRssSyncPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.RSS_SYNC
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class ArtistIndexConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onViewSelect = (view) => {
|
||||
this.props.dispatchSetArtistView(view);
|
||||
};
|
||||
|
||||
onScroll = ({ scrollTop }) => {
|
||||
scrollPositions.artistIndex = scrollTop;
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ArtistIndex
|
||||
{...this.props}
|
||||
onViewSelect={this.onViewSelect}
|
||||
onScroll={this.onScroll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexConnector.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
view: PropTypes.string.isRequired,
|
||||
dispatchSetArtistView: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withScrollPosition(
|
||||
connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexConnector),
|
||||
'artistIndex'
|
||||
);
|
@ -0,0 +1,49 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setArtistFilter } from 'Store/Actions/artistIndexActions';
|
||||
|
||||
function createArtistSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.artist.items,
|
||||
(artist) => {
|
||||
return artist;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.artistIndex.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default function ArtistIndexFilterModal(props) {
|
||||
const sectionItems = useSelector(createArtistSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'artist';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload) => {
|
||||
dispatch(setArtistFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setArtistFilter } from 'Store/Actions/artistIndexActions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.artist.items,
|
||||
(state) => state.artistIndex.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'artistIndex'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetFilter: setArtistFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
@ -1,167 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ArtistIndexFooter.css';
|
||||
|
||||
class ArtistIndexFooter extends PureComponent {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const { artist } = this.props;
|
||||
const count = artist.length;
|
||||
let tracks = 0;
|
||||
let trackFiles = 0;
|
||||
let ended = 0;
|
||||
let continuing = 0;
|
||||
let monitored = 0;
|
||||
let totalFileSize = 0;
|
||||
|
||||
artist.forEach((s) => {
|
||||
const { statistics = {} } = s;
|
||||
|
||||
const {
|
||||
trackCount = 0,
|
||||
trackFileCount = 0,
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
tracks += trackCount;
|
||||
trackFiles += trackFileCount;
|
||||
|
||||
if (s.status === 'ended') {
|
||||
ended++;
|
||||
} else {
|
||||
continuing++;
|
||||
}
|
||||
|
||||
if (s.monitored) {
|
||||
monitored++;
|
||||
}
|
||||
|
||||
totalFileSize += sizeOnDisk;
|
||||
});
|
||||
|
||||
return (
|
||||
<ColorImpairedConsumer>
|
||||
{(enableColorImpairedMode) => {
|
||||
return (
|
||||
<div className={styles.footer}>
|
||||
<div>
|
||||
<div className={styles.legendItem}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.continuing,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
{translate('ContinuingAllTracksDownloaded')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.ended,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
{translate('EndedAllTracksDownloaded')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.missingMonitored,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
{translate('MissingTracksArtistMonitored')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.missingUnmonitored,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
{translate('MissingTracksArtistNotMonitored')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.statistics}>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('Artist')}
|
||||
data={count}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Inactive')}
|
||||
data={ended}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Continuing')}
|
||||
data={continuing}
|
||||
/>
|
||||
</DescriptionList>
|
||||
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('Monitored')}
|
||||
data={monitored}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Unmonitored')}
|
||||
data={count - monitored}
|
||||
/>
|
||||
</DescriptionList>
|
||||
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('Tracks')}
|
||||
data={tracks}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Files')}
|
||||
data={trackFiles}
|
||||
/>
|
||||
</DescriptionList>
|
||||
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('TotalFileSize')}
|
||||
data={formatBytes(totalFileSize)}
|
||||
/>
|
||||
</DescriptionList>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ColorImpairedConsumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexFooter.propTypes = {
|
||||
artist: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexFooter;
|
@ -0,0 +1,169 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||
import ArtistAppState from 'App/State/ArtistAppState';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ArtistIndexFooter.css';
|
||||
|
||||
function createUnoptimizedSelector() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('artist', 'artistIndex'),
|
||||
(artist: ArtistAppState) => {
|
||||
return artist.items.map((s) => {
|
||||
const { monitored, status, statistics } = s;
|
||||
|
||||
return {
|
||||
monitored,
|
||||
status,
|
||||
statistics,
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createArtistSelector() {
|
||||
return createDeepEqualSelector(
|
||||
createUnoptimizedSelector(),
|
||||
(artist) => artist
|
||||
);
|
||||
}
|
||||
|
||||
export default function ArtistIndexFooter() {
|
||||
const artist = useSelector(createArtistSelector());
|
||||
const count = artist.length;
|
||||
let tracks = 0;
|
||||
let trackFiles = 0;
|
||||
let ended = 0;
|
||||
let continuing = 0;
|
||||
let monitored = 0;
|
||||
let totalFileSize = 0;
|
||||
|
||||
artist.forEach((a) => {
|
||||
const { statistics = { trackCount: 0, trackFileCount: 0, sizeOnDisk: 0 } } =
|
||||
a;
|
||||
|
||||
const { trackCount = 0, trackFileCount = 0, sizeOnDisk = 0 } = statistics;
|
||||
|
||||
tracks += trackCount;
|
||||
trackFiles += trackFileCount;
|
||||
|
||||
if (a.status === 'ended') {
|
||||
ended++;
|
||||
} else {
|
||||
continuing++;
|
||||
}
|
||||
|
||||
if (a.monitored) {
|
||||
monitored++;
|
||||
}
|
||||
|
||||
totalFileSize += sizeOnDisk;
|
||||
});
|
||||
|
||||
return (
|
||||
<ColorImpairedConsumer>
|
||||
{(enableColorImpairedMode) => {
|
||||
return (
|
||||
<div className={styles.footer}>
|
||||
<div>
|
||||
<div className={styles.legendItem}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.continuing,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>{translate('ContinuingAllTracksDownloaded')}</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.ended,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>{translate('EndedAllTracksDownloaded')}</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.missingMonitored,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>{translate('MissingTracksArtistMonitored')}</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.missingUnmonitored,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>{translate('MissingTracksArtistNotMonitored')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.statistics}>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem title={translate('Artist')} data={count} />
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Inactive')}
|
||||
data={ended}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Continuing')}
|
||||
data={continuing}
|
||||
/>
|
||||
</DescriptionList>
|
||||
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('Monitored')}
|
||||
data={monitored}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Unmonitored')}
|
||||
data={count - monitored}
|
||||
/>
|
||||
</DescriptionList>
|
||||
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('Tracks')}
|
||||
data={tracks}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Files')}
|
||||
data={trackFiles}
|
||||
/>
|
||||
</DescriptionList>
|
||||
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('TotalFileSize')}
|
||||
data={formatBytes(totalFileSize)}
|
||||
/>
|
||||
</DescriptionList>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ColorImpairedConsumer>
|
||||
);
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector';
|
||||
import ArtistIndexFooter from './ArtistIndexFooter';
|
||||
|
||||
function createUnoptimizedSelector() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('artist', 'artistIndex'),
|
||||
(artist) => {
|
||||
return artist.items.map((s) => {
|
||||
const {
|
||||
monitored,
|
||||
status,
|
||||
statistics
|
||||
} = s;
|
||||
|
||||
return {
|
||||
monitored,
|
||||
status,
|
||||
statistics
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createArtistSelector() {
|
||||
return createDeepEqualSelector(
|
||||
createUnoptimizedSelector(),
|
||||
(artist) => artist
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createArtistSelector(),
|
||||
(artist) => {
|
||||
return {
|
||||
artist
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(ArtistIndexFooter);
|
@ -1,153 +0,0 @@
|
||||
/* 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 * as commandNames from 'Commands/commandNames';
|
||||
import { toggleArtistMonitored } from 'Store/Actions/artistActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector';
|
||||
import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector';
|
||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
|
||||
|
||||
function selectShowSearchAction() {
|
||||
return createSelector(
|
||||
(state) => state.artistIndex,
|
||||
(artistIndex) => {
|
||||
const view = artistIndex.view;
|
||||
|
||||
switch (view) {
|
||||
case 'posters':
|
||||
return artistIndex.posterOptions.showSearchAction;
|
||||
case 'banners':
|
||||
return artistIndex.bannerOptions.showSearchAction;
|
||||
case 'overview':
|
||||
return artistIndex.overviewOptions.showSearchAction;
|
||||
default:
|
||||
return artistIndex.tableOptions.showSearchAction;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createArtistSelector(),
|
||||
createArtistQualityProfileSelector(),
|
||||
createArtistMetadataProfileSelector(),
|
||||
selectShowSearchAction(),
|
||||
createExecutingCommandsSelector(),
|
||||
(
|
||||
artist,
|
||||
qualityProfile,
|
||||
metadataProfile,
|
||||
showSearchAction,
|
||||
executingCommands
|
||||
) => {
|
||||
|
||||
// If an artist is deleted this selector may fire before the parent
|
||||
// selectors, which will result in an undefined artist, if that happens
|
||||
// we want to return early here and again in the render function to avoid
|
||||
// trying to show an artist that has no information available.
|
||||
|
||||
if (!artist) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const isRefreshingArtist = executingCommands.some((command) => {
|
||||
return (
|
||||
command.name === commandNames.REFRESH_ARTIST &&
|
||||
command.body.artistId === artist.id
|
||||
);
|
||||
});
|
||||
|
||||
const isSearchingArtist = executingCommands.some((command) => {
|
||||
return (
|
||||
command.name === commandNames.ARTIST_SEARCH &&
|
||||
command.body.artistId === artist.id
|
||||
);
|
||||
});
|
||||
|
||||
const latestAlbum = _.maxBy(artist.albums, (album) => album.releaseDate);
|
||||
|
||||
return {
|
||||
...artist,
|
||||
qualityProfile,
|
||||
metadataProfile,
|
||||
latestAlbum,
|
||||
showSearchAction,
|
||||
isRefreshingArtist,
|
||||
isSearchingArtist
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchExecuteCommand: executeCommand,
|
||||
toggleArtistMonitored
|
||||
};
|
||||
|
||||
class ArtistIndexItemConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRefreshArtistPress = () => {
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.REFRESH_ARTIST,
|
||||
artistId: this.props.id
|
||||
});
|
||||
};
|
||||
|
||||
onSearchPress = () => {
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.ARTIST_SEARCH,
|
||||
artistId: this.props.id
|
||||
});
|
||||
};
|
||||
|
||||
onMonitoredPress = () => {
|
||||
this.props.toggleArtistMonitored({
|
||||
artistId: this.props.id,
|
||||
monitored: !this.props.monitored
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
component: ItemComponent,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemComponent
|
||||
{...otherProps}
|
||||
id={id}
|
||||
onRefreshArtistPress={this.onRefreshArtistPress}
|
||||
onSearchPress={this.onSearchPress}
|
||||
onMonitoredPress={this.onMonitoredPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexItemConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
component: PropTypes.elementType.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired,
|
||||
toggleArtistMonitored: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistIndexItemConnector);
|
@ -1,271 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ArtistBanner from 'Artist/ArtistBanner';
|
||||
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||
import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ArtistIndexBannerInfo from './ArtistIndexBannerInfo';
|
||||
import styles from './ArtistIndexBanner.css';
|
||||
|
||||
class ArtistIndexBanner extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditArtistPress = () => {
|
||||
this.setState({ isEditArtistModalOpen: true });
|
||||
};
|
||||
|
||||
onEditArtistModalClose = () => {
|
||||
this.setState({ isEditArtistModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteArtistPress = () => {
|
||||
this.setState({
|
||||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteArtistModalClose = () => {
|
||||
this.setState({ isDeleteArtistModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
artistName,
|
||||
monitored,
|
||||
status,
|
||||
foreignArtistId,
|
||||
nextAiring,
|
||||
statistics,
|
||||
images,
|
||||
bannerWidth,
|
||||
bannerHeight,
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showSearchAction,
|
||||
qualityProfile,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
isRefreshingArtist,
|
||||
isSearchingArtist,
|
||||
onRefreshArtistPress,
|
||||
onSearchPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
albumCount,
|
||||
sizeOnDisk,
|
||||
trackCount,
|
||||
trackFileCount,
|
||||
totalTrackCount
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
isEditArtistModalOpen,
|
||||
isDeleteArtistModalOpen
|
||||
} = this.state;
|
||||
|
||||
const link = `/artist/${foreignArtistId}`;
|
||||
|
||||
const elementStyle = {
|
||||
width: `${bannerWidth}px`,
|
||||
height: `${bannerHeight}px`
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.bannerContainer}>
|
||||
<Label className={styles.controls}>
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
name={icons.REFRESH}
|
||||
title={translate('RefreshArtist')}
|
||||
isSpinning={isRefreshingArtist}
|
||||
onPress={onRefreshArtistPress}
|
||||
/>
|
||||
|
||||
{
|
||||
showSearchAction &&
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
name={icons.SEARCH}
|
||||
title={translate('SearchForMonitoredAlbums')}
|
||||
isSpinning={isSearchingArtist}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
}
|
||||
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.EDIT}
|
||||
title={translate('EditArtist')}
|
||||
onPress={this.onEditArtistPress}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{
|
||||
status === 'ended' &&
|
||||
<div
|
||||
className={styles.ended}
|
||||
title={translate('Inactive')}
|
||||
/>
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
style={elementStyle}
|
||||
to={link}
|
||||
>
|
||||
<ArtistBanner
|
||||
className={styles.banner}
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={70}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ArtistIndexProgressBar
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
trackCount={trackCount}
|
||||
trackFileCount={trackFileCount}
|
||||
totalTrackCount={totalTrackCount}
|
||||
posterWidth={bannerWidth}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
/>
|
||||
|
||||
{
|
||||
showTitle &&
|
||||
<div className={styles.title}>
|
||||
{artistName}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showMonitored &&
|
||||
<div className={styles.title}>
|
||||
{monitored ? 'Monitored' : 'Unmonitored'}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showQualityProfile &&
|
||||
<div className={styles.title}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
nextAiring &&
|
||||
<div className={styles.nextAiring}>
|
||||
{
|
||||
getRelativeDate(
|
||||
nextAiring,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true
|
||||
}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<ArtistIndexBannerInfo
|
||||
albumCount={albumCount}
|
||||
sizeOnDisk={sizeOnDisk}
|
||||
qualityProfile={qualityProfile}
|
||||
showQualityProfile={showQualityProfile}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
<EditArtistModalConnector
|
||||
isOpen={isEditArtistModalOpen}
|
||||
artistId={id}
|
||||
onModalClose={this.onEditArtistModalClose}
|
||||
onDeleteArtistPress={this.onDeleteArtistPress}
|
||||
/>
|
||||
|
||||
<DeleteArtistModal
|
||||
isOpen={isDeleteArtistModalOpen}
|
||||
artistId={id}
|
||||
onModalClose={this.onDeleteArtistModalClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexBanner.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
artistName: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
nextAiring: PropTypes.string,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
bannerWidth: PropTypes.number.isRequired,
|
||||
bannerHeight: PropTypes.number.isRequired,
|
||||
detailedProgressBar: PropTypes.bool.isRequired,
|
||||
showTitle: PropTypes.bool.isRequired,
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
showSearchAction: PropTypes.bool.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
isRefreshingArtist: PropTypes.bool.isRequired,
|
||||
isSearchingArtist: PropTypes.bool.isRequired,
|
||||
onRefreshArtistPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ArtistIndexBanner.defaultProps = {
|
||||
statistics: {
|
||||
albumCount: 0,
|
||||
trackCount: 0,
|
||||
trackFileCount: 0,
|
||||
totalTrackCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
export default ArtistIndexBanner;
|
@ -0,0 +1,257 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Statistics } from 'Artist/Artist';
|
||||
import ArtistBanner from 'Artist/ArtistBanner';
|
||||
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||
import ArtistIndexBannerInfo from 'Artist/Index/Banners/ArtistIndexBannerInfo';
|
||||
import createArtistIndexItemSelector from 'Artist/Index/createArtistIndexItemSelector';
|
||||
import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar';
|
||||
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import selectBannerOptions from './selectBannerOptions';
|
||||
import styles from './ArtistIndexBanner.css';
|
||||
|
||||
interface ArtistIndexBannerProps {
|
||||
artistId: number;
|
||||
sortKey: string;
|
||||
bannerWidth: number;
|
||||
bannerHeight: number;
|
||||
}
|
||||
|
||||
function ArtistIndexBanner(props: ArtistIndexBannerProps) {
|
||||
const { artistId, sortKey, bannerWidth, bannerHeight } = props;
|
||||
|
||||
const {
|
||||
artist,
|
||||
qualityProfile,
|
||||
metadataProfile,
|
||||
isRefreshingArtist,
|
||||
isSearchingArtist,
|
||||
} = useSelector(createArtistIndexItemSelector(props.artistId));
|
||||
|
||||
const {
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showNextAlbum,
|
||||
showSearchAction,
|
||||
} = useSelector(selectBannerOptions);
|
||||
|
||||
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
|
||||
useSelector(createUISettingsSelector());
|
||||
|
||||
const {
|
||||
artistName,
|
||||
artistType,
|
||||
monitored,
|
||||
status,
|
||||
path,
|
||||
foreignArtistId,
|
||||
nextAlbum,
|
||||
added,
|
||||
statistics = {} as Statistics,
|
||||
images,
|
||||
tags,
|
||||
} = artist;
|
||||
|
||||
const {
|
||||
albumCount = 0,
|
||||
trackCount = 0,
|
||||
trackFileCount = 0,
|
||||
totalTrackCount = 0,
|
||||
sizeOnDisk = 0,
|
||||
} = statistics;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [hasBannerError, setHasBannerError] = useState(false);
|
||||
const [isEditArtistModalOpen, setIsEditArtistModalOpen] = useState(false);
|
||||
const [isDeleteArtistModalOpen, setIsDeleteArtistModalOpen] = useState(false);
|
||||
|
||||
const onRefreshPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: REFRESH_ARTIST,
|
||||
artistId,
|
||||
})
|
||||
);
|
||||
}, [artistId, dispatch]);
|
||||
|
||||
const onSearchPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: ARTIST_SEARCH,
|
||||
artistId,
|
||||
})
|
||||
);
|
||||
}, [artistId, dispatch]);
|
||||
|
||||
const onBannerLoadError = useCallback(() => {
|
||||
setHasBannerError(true);
|
||||
}, [setHasBannerError]);
|
||||
|
||||
const onBannerLoad = useCallback(() => {
|
||||
setHasBannerError(false);
|
||||
}, [setHasBannerError]);
|
||||
|
||||
const onEditArtistPress = useCallback(() => {
|
||||
setIsEditArtistModalOpen(true);
|
||||
}, [setIsEditArtistModalOpen]);
|
||||
|
||||
const onEditArtistModalClose = useCallback(() => {
|
||||
setIsEditArtistModalOpen(false);
|
||||
}, [setIsEditArtistModalOpen]);
|
||||
|
||||
const onDeleteArtistPress = useCallback(() => {
|
||||
setIsEditArtistModalOpen(false);
|
||||
setIsDeleteArtistModalOpen(true);
|
||||
}, [setIsDeleteArtistModalOpen]);
|
||||
|
||||
const onDeleteArtistModalClose = useCallback(() => {
|
||||
setIsDeleteArtistModalOpen(false);
|
||||
}, [setIsDeleteArtistModalOpen]);
|
||||
|
||||
const link = `/artist/${foreignArtistId}`;
|
||||
|
||||
const elementStyle = {
|
||||
width: `${bannerWidth}px`,
|
||||
height: `${bannerHeight}px`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.bannerContainer}>
|
||||
<Label className={styles.controls}>
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
name={icons.REFRESH}
|
||||
itle={translate('RefreshArtist')}
|
||||
isSpinning={isRefreshingArtist}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
|
||||
{showSearchAction ? (
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
name={icons.SEARCH}
|
||||
title={translate('SearchForMonitoredAlbums')}
|
||||
isSpinning={isSearchingArtist}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.EDIT}
|
||||
title={translate('EditArtist')}
|
||||
onPress={onEditArtistPress}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{status === 'ended' ? (
|
||||
<div className={styles.ended} title={translate('Inactive')} />
|
||||
) : null}
|
||||
|
||||
<Link className={styles.link} style={elementStyle} to={link}>
|
||||
<ArtistBanner
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={70}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
onError={onBannerLoadError}
|
||||
onLoad={onBannerLoad}
|
||||
/>
|
||||
|
||||
{hasBannerError ? (
|
||||
<div className={styles.overlayTitle}>{artistName}</div>
|
||||
) : null}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ArtistIndexProgressBar
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
trackCount={trackCount}
|
||||
trackFileCount={trackFileCount}
|
||||
totalTrackCount={totalTrackCount}
|
||||
posterWidth={bannerWidth}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
/>
|
||||
|
||||
{showTitle ? (
|
||||
<div className={styles.title} title={artistName}>
|
||||
{artistName}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showMonitored ? (
|
||||
<div className={styles.title}>
|
||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showQualityProfile ? (
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showNextAlbum && !!nextAlbum?.releaseDate ? (
|
||||
<div className={styles.nextAlbum} title={translate('NextAlbum')}>
|
||||
{getRelativeDate(
|
||||
nextAlbum.releaseDate,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ArtistIndexBannerInfo
|
||||
artistType={artistType}
|
||||
added={added}
|
||||
albumCount={albumCount}
|
||||
sizeOnDisk={sizeOnDisk}
|
||||
path={path}
|
||||
tags={tags}
|
||||
qualityProfile={qualityProfile}
|
||||
metadataProfile={metadataProfile}
|
||||
showQualityProfile={showQualityProfile}
|
||||
showNextAlbum={showNextAlbum}
|
||||
showRelativeDates={showRelativeDates}
|
||||
sortKey={sortKey}
|
||||
shortDateFormat={shortDateFormat}
|
||||
longDateFormat={longDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
/>
|
||||
|
||||
<EditArtistModalConnector
|
||||
isOpen={isEditArtistModalOpen}
|
||||
artistId={artistId}
|
||||
onModalClose={onEditArtistModalClose}
|
||||
onDeleteArtistPress={onDeleteArtistPress}
|
||||
/>
|
||||
|
||||
<DeleteArtistModal
|
||||
isOpen={isDeleteArtistModalOpen}
|
||||
artistId={artistId}
|
||||
onModalClose={onDeleteArtistModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexBanner;
|
@ -1,115 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import styles from './ArtistIndexBannerInfo.css';
|
||||
|
||||
function ArtistIndexBannerInfo(props) {
|
||||
const {
|
||||
qualityProfile,
|
||||
showQualityProfile,
|
||||
previousAiring,
|
||||
added,
|
||||
albumCount,
|
||||
path,
|
||||
sizeOnDisk,
|
||||
sortKey,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
if (sortKey === 'qualityProfileId' && !showQualityProfile) {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'previousAiring' && previousAiring) {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{
|
||||
getRelativeDate(
|
||||
previousAiring,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true
|
||||
}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'added' && added) {
|
||||
const addedDate = getRelativeDate(
|
||||
added,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: false
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{`Added ${addedDate}`}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'albumCount') {
|
||||
let albums = '1 album';
|
||||
|
||||
if (albumCount === 0) {
|
||||
albums = 'No albums';
|
||||
} else if (albumCount > 1) {
|
||||
albums = `${albumCount} albums`;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{albums}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'path') {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{path}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'sizeOnDisk') {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{formatBytes(sizeOnDisk)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
ArtistIndexBannerInfo.propTypes = {
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
previousAiring: PropTypes.string,
|
||||
added: PropTypes.string,
|
||||
albumCount: PropTypes.number.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
sizeOnDisk: PropTypes.number,
|
||||
sortKey: PropTypes.string.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexBannerInfo;
|
@ -0,0 +1,187 @@
|
||||
import React from 'react';
|
||||
import Album from 'Album/Album';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import MetadataProfile from 'typings/MetadataProfile';
|
||||
import QualityProfile from 'typings/QualityProfile';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ArtistIndexBannerInfo.css';
|
||||
|
||||
interface ArtistIndexBannerInfoProps {
|
||||
artistType?: string;
|
||||
showQualityProfile: boolean;
|
||||
qualityProfile?: QualityProfile;
|
||||
metadataProfile?: MetadataProfile;
|
||||
showNextAlbum: boolean;
|
||||
nextAlbum?: Album;
|
||||
lastAlbum?: Album;
|
||||
added?: string;
|
||||
albumCount: number;
|
||||
path: string;
|
||||
sizeOnDisk?: number;
|
||||
tags?: number[];
|
||||
sortKey: string;
|
||||
showRelativeDates: boolean;
|
||||
shortDateFormat: string;
|
||||
longDateFormat: string;
|
||||
timeFormat: string;
|
||||
}
|
||||
|
||||
function ArtistIndexBannerInfo(props: ArtistIndexBannerInfoProps) {
|
||||
const {
|
||||
artistType,
|
||||
qualityProfile,
|
||||
metadataProfile,
|
||||
showQualityProfile,
|
||||
showNextAlbum,
|
||||
nextAlbum,
|
||||
lastAlbum,
|
||||
added,
|
||||
albumCount,
|
||||
path,
|
||||
sizeOnDisk,
|
||||
tags,
|
||||
sortKey,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
} = props;
|
||||
|
||||
if (sortKey === 'artistType' && artistType) {
|
||||
return (
|
||||
<div className={styles.info} title={translate('ArtistType')}>
|
||||
{artistType}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
sortKey === 'qualityProfileId' &&
|
||||
!showQualityProfile &&
|
||||
!!qualityProfile?.name
|
||||
) {
|
||||
return (
|
||||
<div className={styles.info} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'metadataProfileId' && !!metadataProfile?.name) {
|
||||
return (
|
||||
<div className={styles.info} title={translate('MetadataProfile')}>
|
||||
{metadataProfile.name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'nextAlbum' && !showNextAlbum && !!nextAlbum?.releaseDate) {
|
||||
return (
|
||||
<div
|
||||
className={styles.info}
|
||||
title={`${translate('NextAlbum')}: ${formatDateTime(
|
||||
nextAlbum.releaseDate,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}`}
|
||||
>
|
||||
{getRelativeDate(
|
||||
nextAlbum.releaseDate,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'lastAlbum' && !!lastAlbum?.releaseDate) {
|
||||
return (
|
||||
<div
|
||||
className={styles.info}
|
||||
title={`${translate('LastAlbum')}: ${formatDateTime(
|
||||
lastAlbum.releaseDate,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}`}
|
||||
>
|
||||
{getRelativeDate(
|
||||
lastAlbum.releaseDate,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'added' && added) {
|
||||
const addedDate = getRelativeDate(
|
||||
added,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: false,
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.info}
|
||||
title={formatDateTime(added, longDateFormat, timeFormat)}
|
||||
>
|
||||
{translate('Added')}: {addedDate}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'albumCount') {
|
||||
let albums = translate('OneAlbum');
|
||||
|
||||
if (albumCount === 0) {
|
||||
albums = translate('NoAlbums');
|
||||
} else if (albumCount > 1) {
|
||||
albums = translate('CountAlbums', { albumCount });
|
||||
}
|
||||
|
||||
return <div className={styles.info}>{albums}</div>;
|
||||
}
|
||||
|
||||
if (sortKey === 'path') {
|
||||
return (
|
||||
<div className={styles.info} title={translate('Path')}>
|
||||
{path}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'sizeOnDisk') {
|
||||
return (
|
||||
<div className={styles.info} title={translate('SizeOnDisk')}>
|
||||
{formatBytes(sizeOnDisk)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'tags' && tags) {
|
||||
return (
|
||||
<div className={styles.info} title={translate('Tags')}>
|
||||
<TagListConnector tags={tags} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default ArtistIndexBannerInfo;
|
@ -1,327 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Grid, WindowScroller } from 'react-virtualized';
|
||||
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
||||
import Measure from 'Components/Measure';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import ArtistIndexBanner from './ArtistIndexBanner';
|
||||
import styles from './ArtistIndexBanners.css';
|
||||
|
||||
// container dimensions
|
||||
const columnPadding = parseInt(dimensions.artistIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen);
|
||||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||
|
||||
const additionalColumnCount = {
|
||||
small: 3,
|
||||
medium: 2,
|
||||
large: 1
|
||||
};
|
||||
|
||||
function calculateColumnWidth(width, bannerSize, isSmallScreen) {
|
||||
const maxiumColumnWidth = isSmallScreen ? 344 : 364;
|
||||
const columns = Math.floor(width / maxiumColumnWidth);
|
||||
const remainder = width % maxiumColumnWidth;
|
||||
|
||||
if (remainder === 0 && bannerSize === 'large') {
|
||||
return maxiumColumnWidth;
|
||||
}
|
||||
|
||||
return Math.floor(width / (columns + additionalColumnCount[bannerSize]));
|
||||
}
|
||||
|
||||
function calculateRowHeight(bannerHeight, sortKey, isSmallScreen, bannerOptions) {
|
||||
const {
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile
|
||||
} = bannerOptions;
|
||||
|
||||
const nextAiringHeight = 19;
|
||||
|
||||
const heights = [
|
||||
bannerHeight,
|
||||
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
|
||||
nextAiringHeight,
|
||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||
];
|
||||
|
||||
if (showTitle) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showMonitored) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showQualityProfile) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
switch (sortKey) {
|
||||
case 'seasons':
|
||||
case 'previousAiring':
|
||||
case 'added':
|
||||
case 'path':
|
||||
case 'sizeOnDisk':
|
||||
heights.push(19);
|
||||
break;
|
||||
case 'qualityProfileId':
|
||||
if (!showQualityProfile) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// No need to add a height of 0
|
||||
}
|
||||
|
||||
return heights.reduce((acc, height) => acc + height, 0);
|
||||
}
|
||||
|
||||
function calculateHeight(bannerWidth) {
|
||||
return Math.ceil((88/476) * bannerWidth);
|
||||
}
|
||||
|
||||
class ArtistIndexBanners extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
width: 0,
|
||||
columnWidth: 364,
|
||||
columnCount: 1,
|
||||
bannerWidth: 476,
|
||||
bannerHeight: 88,
|
||||
rowHeight: calculateRowHeight(88, null, props.isSmallScreen, {}),
|
||||
scrollRestored: false
|
||||
};
|
||||
|
||||
this._isInitialized = false;
|
||||
this._grid = null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
bannerOptions,
|
||||
jumpToCharacter,
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
columnWidth,
|
||||
columnCount,
|
||||
rowHeight,
|
||||
scrollRestored
|
||||
} = this.state;
|
||||
|
||||
if (prevProps.sortKey !== sortKey ||
|
||||
prevProps.bannerOptions !== bannerOptions) {
|
||||
this.calculateGrid();
|
||||
}
|
||||
|
||||
if (this._grid &&
|
||||
(prevState.width !== width ||
|
||||
prevState.columnWidth !== columnWidth ||
|
||||
prevState.columnCount !== columnCount ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items))) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
||||
this.setState({ scrollRestored: true });
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
|
||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (this._grid && index != null) {
|
||||
const row = Math.floor(index / columnCount);
|
||||
|
||||
this._grid.scrollToCell({
|
||||
rowIndex: row,
|
||||
columnIndex: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setGridRef = (ref) => {
|
||||
this._grid = ref;
|
||||
};
|
||||
|
||||
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||
const {
|
||||
sortKey,
|
||||
bannerOptions
|
||||
} = this.props;
|
||||
|
||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||
const columnWidth = calculateColumnWidth(width, bannerOptions.size, isSmallScreen);
|
||||
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
|
||||
const bannerWidth = columnWidth - padding;
|
||||
const bannerHeight = calculateHeight(bannerWidth);
|
||||
const rowHeight = calculateRowHeight(bannerHeight, sortKey, isSmallScreen, bannerOptions);
|
||||
|
||||
this.setState({
|
||||
width,
|
||||
columnWidth,
|
||||
columnCount,
|
||||
bannerWidth,
|
||||
bannerHeight,
|
||||
rowHeight
|
||||
});
|
||||
};
|
||||
|
||||
cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
bannerOptions,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bannerWidth,
|
||||
bannerHeight,
|
||||
columnCount
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile
|
||||
} = bannerOptions;
|
||||
|
||||
const artist = items[rowIndex * columnCount + columnIndex];
|
||||
|
||||
if (!artist) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
key={key}
|
||||
>
|
||||
<ArtistIndexItemConnector
|
||||
key={artist.id}
|
||||
component={ArtistIndexBanner}
|
||||
sortKey={sortKey}
|
||||
bannerWidth={bannerWidth}
|
||||
bannerHeight={bannerHeight}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
showTitle={showTitle}
|
||||
showMonitored={showMonitored}
|
||||
showQualityProfile={showQualityProfile}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
artistId={artist.id}
|
||||
qualityProfileId={artist.qualityProfileId}
|
||||
metadataProfileId={artist.metadataProfileId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMeasure = ({ width }) => {
|
||||
this.calculateGrid(width, this.props.isSmallScreen);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
isSmallScreen,
|
||||
scroller
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
columnWidth,
|
||||
columnCount,
|
||||
rowHeight
|
||||
} = this.state;
|
||||
|
||||
const rowCount = Math.ceil(items.length / columnCount);
|
||||
|
||||
return (
|
||||
<Measure
|
||||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<WindowScroller
|
||||
scrollElement={isSmallScreen ? undefined : scroller}
|
||||
>
|
||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||
if (!height) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid
|
||||
ref={this.setGridRef}
|
||||
className={styles.grid}
|
||||
autoHeight={true}
|
||||
height={height}
|
||||
columnCount={columnCount}
|
||||
columnWidth={columnWidth}
|
||||
rowCount={rowCount}
|
||||
rowHeight={rowHeight}
|
||||
width={width}
|
||||
onScroll={onChildScroll}
|
||||
scrollTop={scrollTop}
|
||||
overscanRowCount={2}
|
||||
cellRenderer={this.cellRenderer}
|
||||
scrollToAlignment={'start'}
|
||||
isScrollingOptOut={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
</WindowScroller>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexBanners.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
bannerOptions: PropTypes.object.isRequired,
|
||||
jumpToCharacter: PropTypes.string,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexBanners;
|
@ -0,0 +1,294 @@
|
||||
import { throttle } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
|
||||
import { createSelector } from 'reselect';
|
||||
import Artist from 'Artist/Artist';
|
||||
import ArtistIndexBanner from 'Artist/Index/Banners/ArtistIndexBanner';
|
||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
|
||||
const bodyPadding = parseInt(dimensions.pageContentBodyPadding);
|
||||
const bodyPaddingSmallScreen = parseInt(
|
||||
dimensions.pageContentBodyPaddingSmallScreen
|
||||
);
|
||||
const columnPadding = parseInt(dimensions.artistIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(
|
||||
dimensions.artistIndexColumnPaddingSmallScreen
|
||||
);
|
||||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||
|
||||
const ADDITIONAL_COLUMN_COUNT = {
|
||||
small: 3,
|
||||
medium: 2,
|
||||
large: 1,
|
||||
};
|
||||
|
||||
interface CellItemData {
|
||||
layout: {
|
||||
columnCount: number;
|
||||
padding: number;
|
||||
bannerWidth: number;
|
||||
bannerHeight: number;
|
||||
};
|
||||
items: Artist[];
|
||||
sortKey: string;
|
||||
}
|
||||
|
||||
interface ArtistIndexBannersProps {
|
||||
items: Artist[];
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
const artistIndexSelector = createSelector(
|
||||
(state) => state.artistIndex.bannerOptions,
|
||||
(bannerOptions) => {
|
||||
return {
|
||||
bannerOptions,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
style,
|
||||
data,
|
||||
}) => {
|
||||
const { layout, items, sortKey } = data;
|
||||
|
||||
const { columnCount, padding, bannerWidth, bannerHeight } = layout;
|
||||
|
||||
const index = rowIndex * columnCount + columnIndex;
|
||||
|
||||
if (index >= items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const artist = items[index];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<ArtistIndexBanner
|
||||
artistId={artist.id}
|
||||
sortKey={sortKey}
|
||||
bannerWidth={bannerWidth}
|
||||
bannerHeight={bannerHeight}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getWindowScrollTopPosition() {
|
||||
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||
}
|
||||
|
||||
export default function ArtistIndexBanners(props: ArtistIndexBannersProps) {
|
||||
const { scrollerRef, items, sortKey, jumpToCharacter, isSmallScreen } = props;
|
||||
|
||||
const { bannerOptions } = useSelector(artistIndexSelector);
|
||||
const ref: React.MutableRefObject<Grid> = useRef();
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
const columnWidth = useMemo(() => {
|
||||
const { width } = size;
|
||||
const maximumColumnWidth = isSmallScreen ? 344 : 364;
|
||||
const columns = Math.floor(width / maximumColumnWidth);
|
||||
const remainder = width % maximumColumnWidth;
|
||||
return remainder === 0
|
||||
? maximumColumnWidth
|
||||
: Math.floor(
|
||||
width / (columns + ADDITIONAL_COLUMN_COUNT[bannerOptions.size])
|
||||
);
|
||||
}, [isSmallScreen, bannerOptions, size]);
|
||||
|
||||
const columnCount = useMemo(
|
||||
() => Math.max(Math.floor(size.width / columnWidth), 1),
|
||||
[size, columnWidth]
|
||||
);
|
||||
const padding = props.isSmallScreen
|
||||
? columnPaddingSmallScreen
|
||||
: columnPadding;
|
||||
const bannerWidth = columnWidth - padding * 2;
|
||||
const bannerHeight = Math.ceil((88 / 476) * bannerWidth);
|
||||
|
||||
const rowHeight = useMemo(() => {
|
||||
const {
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showNextAlbum,
|
||||
} = bannerOptions;
|
||||
|
||||
const nextAiringHeight = 19;
|
||||
|
||||
const heights = [
|
||||
bannerHeight,
|
||||
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
|
||||
nextAiringHeight,
|
||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding,
|
||||
];
|
||||
|
||||
if (showTitle) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showMonitored) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showQualityProfile) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showNextAlbum) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
switch (sortKey) {
|
||||
case 'artistType':
|
||||
case 'metadataProfileId':
|
||||
case 'lastAlbum':
|
||||
case 'added':
|
||||
case 'albumCount':
|
||||
case 'path':
|
||||
case 'sizeOnDisk':
|
||||
case 'tags':
|
||||
heights.push(19);
|
||||
break;
|
||||
case 'qualityProfileId':
|
||||
if (!showQualityProfile) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
case 'nextAlbum':
|
||||
if (!showNextAlbum) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// No need to add a height of 0
|
||||
}
|
||||
|
||||
return heights.reduce((acc, height) => acc + height, 0);
|
||||
}, [isSmallScreen, bannerOptions, sortKey, bannerHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
const current = scrollerRef.current;
|
||||
|
||||
if (isSmallScreen) {
|
||||
const padding = bodyPaddingSmallScreen - 5;
|
||||
|
||||
setSize({
|
||||
width: window.innerWidth - padding * 2,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (current) {
|
||||
const width = current.clientWidth;
|
||||
const padding = bodyPadding - 5;
|
||||
|
||||
setSize({
|
||||
width: width - padding * 2,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
}, [isSmallScreen, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
const scrollTop =
|
||||
(isSmallScreen
|
||||
? getWindowScrollTopPosition()
|
||||
: currentScrollerRef.scrollTop) - offsetTop;
|
||||
|
||||
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
}, 10);
|
||||
|
||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
handleScroll.cancel();
|
||||
|
||||
if (currentScrollListener) {
|
||||
currentScrollListener.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
}, [isSmallScreen, ref, scrollerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (jumpToCharacter) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (index != null) {
|
||||
const rowIndex = Math.floor(index / columnCount);
|
||||
|
||||
const scrollTop = rowIndex * rowHeight + padding;
|
||||
|
||||
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
scrollerRef.current.scrollTo(0, scrollTop);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
jumpToCharacter,
|
||||
rowHeight,
|
||||
columnCount,
|
||||
padding,
|
||||
items,
|
||||
scrollerRef,
|
||||
ref,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div ref={measureRef}>
|
||||
<Grid<CellItemData>
|
||||
ref={ref}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'none',
|
||||
}}
|
||||
width={size.width}
|
||||
height={size.height}
|
||||
columnCount={columnCount}
|
||||
columnWidth={columnWidth}
|
||||
rowCount={Math.ceil(items.length / columnCount)}
|
||||
rowHeight={rowHeight}
|
||||
itemData={{
|
||||
layout: {
|
||||
columnCount,
|
||||
padding,
|
||||
bannerWidth,
|
||||
bannerHeight,
|
||||
},
|
||||
items,
|
||||
sortKey,
|
||||
}}
|
||||
>
|
||||
{Cell}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import ArtistIndexBanners from './ArtistIndexBanners';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.artistIndex.bannerOptions,
|
||||
createUISettingsSelector(),
|
||||
createDimensionsSelector(),
|
||||
(bannerOptions, uiSettings, dimensions) => {
|
||||
return {
|
||||
bannerOptions,
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(ArtistIndexBanners);
|
@ -1,25 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ArtistIndexBannerOptionsModalContentConnector from './ArtistIndexBannerOptionsModalContentConnector';
|
||||
|
||||
function ArtistIndexBannerOptionsModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ArtistIndexBannerOptionsModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ArtistIndexBannerOptionsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexBannerOptionsModal;
|
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ArtistIndexBannerOptionsModalContent from './ArtistIndexBannerOptionsModalContent';
|
||||
|
||||
interface ArtistIndexBannerOptionsModalProps {
|
||||
isOpen: boolean;
|
||||
onModalClose(...args: unknown[]): unknown;
|
||||
}
|
||||
|
||||
function ArtistIndexBannerOptionsModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
}: ArtistIndexBannerOptionsModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<ArtistIndexBannerOptionsModalContent onModalClose={onModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexBannerOptionsModal;
|
@ -1,226 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const bannerSizeOptions = [
|
||||
{ key: 'small', value: 'Small' },
|
||||
{ key: 'medium', value: 'Medium' },
|
||||
{ key: 'large', value: 'Large' }
|
||||
];
|
||||
|
||||
class ArtistIndexBannerOptionsModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
detailedProgressBar: props.detailedProgressBar,
|
||||
size: props.size,
|
||||
showTitle: props.showTitle,
|
||||
showMonitored: props.showMonitored,
|
||||
showQualityProfile: props.showQualityProfile,
|
||||
showSearchAction: props.showSearchAction
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
detailedProgressBar,
|
||||
size,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showSearchAction
|
||||
} = this.props;
|
||||
|
||||
const state = {};
|
||||
|
||||
if (detailedProgressBar !== prevProps.detailedProgressBar) {
|
||||
state.detailedProgressBar = detailedProgressBar;
|
||||
}
|
||||
|
||||
if (size !== prevProps.size) {
|
||||
state.size = size;
|
||||
}
|
||||
|
||||
if (showTitle !== prevProps.showTitle) {
|
||||
state.showTitle = showTitle;
|
||||
}
|
||||
|
||||
if (showMonitored !== prevProps.showMonitored) {
|
||||
state.showMonitored = showMonitored;
|
||||
}
|
||||
|
||||
if (showQualityProfile !== prevProps.showQualityProfile) {
|
||||
state.showQualityProfile = showQualityProfile;
|
||||
}
|
||||
|
||||
if (showSearchAction !== prevProps.showSearchAction) {
|
||||
state.showSearchAction = showSearchAction;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(state)) {
|
||||
this.setState(state);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChangeBannerOption = ({ name, value }) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
}, () => {
|
||||
this.props.onChangeBannerOption({ [name]: value });
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
detailedProgressBar,
|
||||
size,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showSearchAction
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Options
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Size')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="size"
|
||||
value={size}
|
||||
values={bannerSizeOptions}
|
||||
onChange={this.onChangeBannerOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('DetailedProgressBar')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="detailedProgressBar"
|
||||
value={detailedProgressBar}
|
||||
helpText={translate('DetailedProgressBarHelpText')}
|
||||
onChange={this.onChangeBannerOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowName')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showTitle"
|
||||
value={showTitle}
|
||||
helpText={translate('ShowTitleHelpText')}
|
||||
onChange={this.onChangeBannerOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowMonitored')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showMonitored"
|
||||
value={showMonitored}
|
||||
helpText={translate('ShowMonitoredHelpText')}
|
||||
onChange={this.onChangeBannerOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowQualityProfile')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showQualityProfile"
|
||||
value={showQualityProfile}
|
||||
helpText={translate('ShowQualityProfileHelpText')}
|
||||
onChange={this.onChangeBannerOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowSearch')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showSearchAction"
|
||||
value={showSearchAction}
|
||||
helpText={translate('ShowSearchActionHelpText')}
|
||||
onChange={this.onChangeBannerOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexBannerOptionsModalContent.propTypes = {
|
||||
size: PropTypes.string.isRequired,
|
||||
showTitle: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
detailedProgressBar: PropTypes.bool.isRequired,
|
||||
showSearchAction: PropTypes.bool.isRequired,
|
||||
onChangeBannerOption: PropTypes.func.isRequired,
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexBannerOptionsModalContent;
|
@ -0,0 +1,167 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import selectBannerOptions from 'Artist/Index/Banners/selectBannerOptions';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { setArtistBannerOption } from 'Store/Actions/artistIndexActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const bannerSizeOptions = [
|
||||
{
|
||||
key: 'small',
|
||||
get value() {
|
||||
return translate('Small');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'medium',
|
||||
get value() {
|
||||
return translate('Medium');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'large',
|
||||
get value() {
|
||||
return translate('Large');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
interface ArtistIndexBannerOptionsModalContentProps {
|
||||
onModalClose(...args: unknown[]): unknown;
|
||||
}
|
||||
|
||||
function ArtistIndexBannerOptionsModalContent(
|
||||
props: ArtistIndexBannerOptionsModalContentProps
|
||||
) {
|
||||
const { onModalClose } = props;
|
||||
|
||||
const bannerOptions = useSelector(selectBannerOptions);
|
||||
|
||||
const {
|
||||
detailedProgressBar,
|
||||
size,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showNextAlbum,
|
||||
showSearchAction,
|
||||
} = bannerOptions;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onBannerOptionChange = useCallback(
|
||||
({ name, value }) => {
|
||||
dispatch(setArtistBannerOption({ [name]: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('BannerOptions')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('BannerSize')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="size"
|
||||
value={size}
|
||||
values={bannerSizeOptions}
|
||||
onChange={onBannerOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('DetailedProgressBar')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="detailedProgressBar"
|
||||
value={detailedProgressBar}
|
||||
helpText={translate('DetailedProgressBarHelpText')}
|
||||
onChange={onBannerOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowName')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showTitle"
|
||||
value={showTitle}
|
||||
helpText={translate('ShowTitleHelpText')}
|
||||
onChange={onBannerOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowMonitored')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showMonitored"
|
||||
value={showMonitored}
|
||||
helpText={translate('ShowMonitoredHelpText')}
|
||||
onChange={onBannerOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowQualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showQualityProfile"
|
||||
value={showQualityProfile}
|
||||
helpText={translate('ShowQualityProfileHelpText')}
|
||||
onChange={onBannerOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowNextAlbum')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showNextAlbum"
|
||||
value={showNextAlbum}
|
||||
helpText={translate('ShowNextAlbumHelpText')}
|
||||
onChange={onBannerOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showSearchAction"
|
||||
value={showSearchAction}
|
||||
helpText={translate('ShowSearchActionHelpText')}
|
||||
onChange={onBannerOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexBannerOptionsModalContent;
|
@ -1,23 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setArtistBannerOption } from 'Store/Actions/artistIndexActions';
|
||||
import ArtistIndexBannerOptionsModalContent from './ArtistIndexBannerOptionsModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.artistIndex,
|
||||
(artistIndex) => {
|
||||
return artistIndex.bannerOptions;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onChangeBannerOption(payload) {
|
||||
dispatch(setArtistBannerOption(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexBannerOptionsModalContent);
|
@ -0,0 +1,9 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
const selectBannerOptions = createSelector(
|
||||
(state: AppState) => state.artistIndex.bannerOptions,
|
||||
(bannerOptions) => bannerOptions
|
||||
);
|
||||
|
||||
export default selectBannerOptions;
|
@ -1,283 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import ArtistPoster from 'Artist/ArtistPoster';
|
||||
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||
import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ArtistIndexOverviewInfo from './ArtistIndexOverviewInfo';
|
||||
import styles from './ArtistIndexOverview.css';
|
||||
|
||||
const columnPadding = parseInt(dimensions.artistIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen);
|
||||
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||
const lineHeight = parseFloat(fonts.lineHeight);
|
||||
|
||||
// Hardcoded height beased on line-height of 32 + bottom margin of 10.
|
||||
// Less side-effecty than using react-measure.
|
||||
const titleRowHeight = 42;
|
||||
|
||||
function getContentHeight(rowHeight, isSmallScreen) {
|
||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||
|
||||
return rowHeight - padding;
|
||||
}
|
||||
|
||||
class ArtistIndexOverview extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditArtistPress = () => {
|
||||
this.setState({ isEditArtistModalOpen: true });
|
||||
};
|
||||
|
||||
onEditArtistModalClose = () => {
|
||||
this.setState({ isEditArtistModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteArtistPress = () => {
|
||||
this.setState({
|
||||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteArtistModalClose = () => {
|
||||
this.setState({ isDeleteArtistModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
artistName,
|
||||
overview,
|
||||
monitored,
|
||||
status,
|
||||
foreignArtistId,
|
||||
nextAiring,
|
||||
statistics,
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
qualityProfile,
|
||||
overviewOptions,
|
||||
showSearchAction,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
rowHeight,
|
||||
isSmallScreen,
|
||||
isRefreshingArtist,
|
||||
isSearchingArtist,
|
||||
onRefreshArtistPress,
|
||||
onSearchPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
albumCount,
|
||||
sizeOnDisk,
|
||||
trackCount,
|
||||
trackFileCount,
|
||||
totalTrackCount
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
isEditArtistModalOpen,
|
||||
isDeleteArtistModalOpen
|
||||
} = this.state;
|
||||
|
||||
const link = `/artist/${foreignArtistId}`;
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}px`
|
||||
};
|
||||
|
||||
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
||||
const overviewHeight = contentHeight - titleRowHeight;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.poster}>
|
||||
<div className={styles.posterContainer}>
|
||||
{
|
||||
status === 'ended' &&
|
||||
<div
|
||||
className={styles.ended}
|
||||
title={translate('Inactive')}
|
||||
/>
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
style={elementStyle}
|
||||
to={link}
|
||||
>
|
||||
<ArtistPoster
|
||||
className={styles.poster}
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ArtistIndexProgressBar
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
trackCount={trackCount}
|
||||
trackFileCount={trackFileCount}
|
||||
totalTrackCount={totalTrackCount}
|
||||
posterWidth={posterWidth}
|
||||
detailedProgressBar={overviewOptions.detailedProgressBar}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.info} style={{ maxHeight: contentHeight }}>
|
||||
<div className={styles.titleRow}>
|
||||
<Link
|
||||
className={styles.title}
|
||||
to={link}
|
||||
>
|
||||
{artistName}
|
||||
</Link>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<SpinnerIconButton
|
||||
name={icons.REFRESH}
|
||||
title={translate('RefreshArtist')}
|
||||
isSpinning={isRefreshingArtist}
|
||||
onPress={onRefreshArtistPress}
|
||||
/>
|
||||
|
||||
{
|
||||
showSearchAction &&
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
name={icons.SEARCH}
|
||||
title={translate('SearchForMonitoredAlbums')}
|
||||
isSpinning={isSearchingArtist}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
}
|
||||
|
||||
<IconButton
|
||||
name={icons.EDIT}
|
||||
title={translate('EditArtist')}
|
||||
onPress={this.onEditArtistPress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.details}>
|
||||
|
||||
<Link
|
||||
className={styles.overview}
|
||||
to={link}
|
||||
>
|
||||
<TextTruncate
|
||||
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
||||
text={overview}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<ArtistIndexOverviewInfo
|
||||
height={overviewHeight}
|
||||
monitored={monitored}
|
||||
nextAiring={nextAiring}
|
||||
albumCount={albumCount}
|
||||
sizeOnDisk={sizeOnDisk}
|
||||
qualityProfile={qualityProfile}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
longDateFormat={longDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
{...overviewOptions}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditArtistModalConnector
|
||||
isOpen={isEditArtistModalOpen}
|
||||
artistId={id}
|
||||
onModalClose={this.onEditArtistModalClose}
|
||||
onDeleteArtistPress={this.onDeleteArtistPress}
|
||||
/>
|
||||
|
||||
<DeleteArtistModal
|
||||
isOpen={isDeleteArtistModalOpen}
|
||||
artistId={id}
|
||||
onModalClose={this.onDeleteArtistModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexOverview.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
artistName: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
nextAiring: PropTypes.string,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
posterWidth: PropTypes.number.isRequired,
|
||||
posterHeight: PropTypes.number.isRequired,
|
||||
rowHeight: PropTypes.number.isRequired,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
overviewOptions: PropTypes.object.isRequired,
|
||||
showSearchAction: PropTypes.bool.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
isRefreshingArtist: PropTypes.bool.isRequired,
|
||||
isSearchingArtist: PropTypes.bool.isRequired,
|
||||
onRefreshArtistPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ArtistIndexOverview.defaultProps = {
|
||||
statistics: {
|
||||
albumCount: 0,
|
||||
trackCount: 0,
|
||||
trackFileCount: 0,
|
||||
totalTrackCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
export default ArtistIndexOverview;
|
@ -0,0 +1,240 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import { Statistics } from 'Artist/Artist';
|
||||
import ArtistPoster from 'Artist/ArtistPoster';
|
||||
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||
import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar';
|
||||
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import createArtistIndexItemSelector from '../createArtistIndexItemSelector';
|
||||
import ArtistIndexOverviewInfo from './ArtistIndexOverviewInfo';
|
||||
import selectOverviewOptions from './selectOverviewOptions';
|
||||
import styles from './ArtistIndexOverview.css';
|
||||
|
||||
const columnPadding = parseInt(dimensions.artistIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(
|
||||
dimensions.artistIndexColumnPaddingSmallScreen
|
||||
);
|
||||
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||
const lineHeight = parseFloat(fonts.lineHeight);
|
||||
|
||||
// Hardcoded height based on line-height of 32 + bottom margin of 10.
|
||||
// Less side-effecty than using react-measure.
|
||||
const TITLE_HEIGHT = 42;
|
||||
|
||||
interface ArtistIndexOverviewProps {
|
||||
artistId: number;
|
||||
sortKey: string;
|
||||
posterWidth: number;
|
||||
posterHeight: number;
|
||||
rowHeight: number;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
function ArtistIndexOverview(props: ArtistIndexOverviewProps) {
|
||||
const {
|
||||
artistId,
|
||||
sortKey,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
rowHeight,
|
||||
isSmallScreen,
|
||||
} = props;
|
||||
|
||||
const { artist, qualityProfile, isRefreshingArtist, isSearchingArtist } =
|
||||
useSelector(createArtistIndexItemSelector(props.artistId));
|
||||
|
||||
const overviewOptions = useSelector(selectOverviewOptions);
|
||||
|
||||
const {
|
||||
artistName,
|
||||
monitored,
|
||||
status,
|
||||
path,
|
||||
foreignArtistId,
|
||||
nextAlbum,
|
||||
lastAlbum,
|
||||
added,
|
||||
overview,
|
||||
statistics = {} as Statistics,
|
||||
images,
|
||||
} = artist;
|
||||
|
||||
const {
|
||||
albumCount = 0,
|
||||
sizeOnDisk = 0,
|
||||
trackCount = 0,
|
||||
trackFileCount = 0,
|
||||
totalTrackCount = 0,
|
||||
} = statistics;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [isEditArtistModalOpen, setIsEditArtistModalOpen] = useState(false);
|
||||
const [isDeleteArtistModalOpen, setIsDeleteArtistModalOpen] = useState(false);
|
||||
|
||||
const onRefreshPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: REFRESH_ARTIST,
|
||||
artistId,
|
||||
})
|
||||
);
|
||||
}, [artistId, dispatch]);
|
||||
|
||||
const onSearchPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: ARTIST_SEARCH,
|
||||
artistId,
|
||||
})
|
||||
);
|
||||
}, [artistId, dispatch]);
|
||||
|
||||
const onEditArtistPress = useCallback(() => {
|
||||
setIsEditArtistModalOpen(true);
|
||||
}, [setIsEditArtistModalOpen]);
|
||||
|
||||
const onEditArtistModalClose = useCallback(() => {
|
||||
setIsEditArtistModalOpen(false);
|
||||
}, [setIsEditArtistModalOpen]);
|
||||
|
||||
const onDeleteArtistPress = useCallback(() => {
|
||||
setIsEditArtistModalOpen(false);
|
||||
setIsDeleteArtistModalOpen(true);
|
||||
}, [setIsDeleteArtistModalOpen]);
|
||||
|
||||
const onDeleteArtistModalClose = useCallback(() => {
|
||||
setIsDeleteArtistModalOpen(false);
|
||||
}, [setIsDeleteArtistModalOpen]);
|
||||
|
||||
const link = `/artist/${foreignArtistId}`;
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}px`,
|
||||
};
|
||||
|
||||
const contentHeight = useMemo(() => {
|
||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||
|
||||
return rowHeight - padding;
|
||||
}, [rowHeight, isSmallScreen]);
|
||||
|
||||
const overviewHeight = contentHeight - TITLE_HEIGHT;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.poster}>
|
||||
<div className={styles.posterContainer}>
|
||||
{status === 'ended' && (
|
||||
<div className={styles.ended} title={translate('Inactive')} />
|
||||
)}
|
||||
|
||||
<Link className={styles.link} style={elementStyle} to={link}>
|
||||
<ArtistPoster
|
||||
className={styles.poster}
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ArtistIndexProgressBar
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
trackCount={trackCount}
|
||||
trackFileCount={trackFileCount}
|
||||
totalTrackCount={totalTrackCount}
|
||||
posterWidth={posterWidth}
|
||||
detailedProgressBar={overviewOptions.detailedProgressBar}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.info} style={{ maxHeight: contentHeight }}>
|
||||
<div className={styles.titleRow}>
|
||||
<Link className={styles.title} to={link}>
|
||||
{artistName}
|
||||
</Link>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<SpinnerIconButton
|
||||
name={icons.REFRESH}
|
||||
title={translate('RefreshArtist')}
|
||||
isSpinning={isRefreshingArtist}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
|
||||
{overviewOptions.showSearchAction ? (
|
||||
<SpinnerIconButton
|
||||
name={icons.SEARCH}
|
||||
title={translate('SearchForMonitoredAlbums')}
|
||||
isSpinning={isSearchingArtist}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<IconButton
|
||||
name={icons.EDIT}
|
||||
title={translate('EditArtist')}
|
||||
onPress={onEditArtistPress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.details}>
|
||||
<Link className={styles.overview} to={link}>
|
||||
<TextTruncate
|
||||
line={Math.floor(
|
||||
overviewHeight / (defaultFontSize * lineHeight)
|
||||
)}
|
||||
text={overview}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<ArtistIndexOverviewInfo
|
||||
height={overviewHeight}
|
||||
monitored={monitored}
|
||||
nextAlbum={nextAlbum}
|
||||
lastAlbum={lastAlbum}
|
||||
added={added}
|
||||
albumCount={albumCount}
|
||||
qualityProfile={qualityProfile}
|
||||
sizeOnDisk={sizeOnDisk}
|
||||
path={path}
|
||||
sortKey={sortKey}
|
||||
{...overviewOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditArtistModalConnector
|
||||
isOpen={isEditArtistModalOpen}
|
||||
artistId={artistId}
|
||||
onModalClose={onEditArtistModalClose}
|
||||
onDeleteArtistPress={onDeleteArtistPress}
|
||||
/>
|
||||
|
||||
<DeleteArtistModal
|
||||
isOpen={isDeleteArtistModalOpen}
|
||||
artistId={artistId}
|
||||
onModalClose={onDeleteArtistModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexOverview;
|
@ -1,249 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import ArtistIndexOverviewInfoRow from './ArtistIndexOverviewInfoRow';
|
||||
import styles from './ArtistIndexOverviewInfo.css';
|
||||
|
||||
const infoRowHeight = parseInt(dimensions.artistIndexOverviewInfoRowHeight);
|
||||
|
||||
const rows = [
|
||||
{
|
||||
name: 'monitored',
|
||||
showProp: 'showMonitored',
|
||||
valueProp: 'monitored'
|
||||
},
|
||||
{
|
||||
name: 'qualityProfileId',
|
||||
showProp: 'showQualityProfile',
|
||||
valueProp: 'qualityProfileId'
|
||||
},
|
||||
{
|
||||
name: 'lastAlbum',
|
||||
showProp: 'showLastAlbum',
|
||||
valueProp: 'lastAlbum'
|
||||
},
|
||||
{
|
||||
name: 'added',
|
||||
showProp: 'showAdded',
|
||||
valueProp: 'added'
|
||||
},
|
||||
{
|
||||
name: 'albumCount',
|
||||
showProp: 'showAlbumCount',
|
||||
valueProp: 'albumCount'
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
showProp: 'showPath',
|
||||
valueProp: 'path'
|
||||
},
|
||||
{
|
||||
name: 'sizeOnDisk',
|
||||
showProp: 'showSizeOnDisk',
|
||||
valueProp: 'sizeOnDisk'
|
||||
}
|
||||
];
|
||||
|
||||
function isVisible(row, props) {
|
||||
const {
|
||||
name,
|
||||
showProp,
|
||||
valueProp
|
||||
} = row;
|
||||
|
||||
if (props[valueProp] == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return props[showProp] || props.sortKey === name;
|
||||
}
|
||||
|
||||
function getInfoRowProps(row, props) {
|
||||
const { name } = row;
|
||||
|
||||
if (name === 'monitored') {
|
||||
const monitoredText = props.monitored ? 'Monitored' : 'Unmonitored';
|
||||
|
||||
return {
|
||||
title: monitoredText,
|
||||
iconName: props.monitored ? icons.MONITORED : icons.UNMONITORED,
|
||||
label: monitoredText
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
return {
|
||||
title: 'Quality Profile',
|
||||
iconName: icons.PROFILE,
|
||||
label: props.qualityProfile.name
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'lastAlbum') {
|
||||
const {
|
||||
lastAlbum,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
return {
|
||||
title: `Last Album: ${lastAlbum.title}`,
|
||||
iconName: icons.CALENDAR,
|
||||
label: getRelativeDate(
|
||||
lastAlbum.releaseDate,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true
|
||||
}
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
const {
|
||||
added,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
return {
|
||||
title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`,
|
||||
iconName: icons.ADD,
|
||||
label: getRelativeDate(
|
||||
added,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true
|
||||
}
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'albumCount') {
|
||||
const { albumCount } = props;
|
||||
let albums = '1 album';
|
||||
|
||||
if (albumCount === 0) {
|
||||
albums = 'No albums';
|
||||
} else if (albumCount > 1) {
|
||||
albums = `${albumCount} albums`;
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Album Count',
|
||||
iconName: icons.CIRCLE,
|
||||
label: albums
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'path') {
|
||||
return {
|
||||
title: 'Path',
|
||||
iconName: icons.FOLDER,
|
||||
label: props.path
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'sizeOnDisk') {
|
||||
return {
|
||||
title: 'Size on Disk',
|
||||
iconName: icons.DRIVE,
|
||||
label: formatBytes(props.sizeOnDisk)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function ArtistIndexOverviewInfo(props) {
|
||||
const {
|
||||
height,
|
||||
nextAiring,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
let shownRows = 1;
|
||||
|
||||
const maxRows = Math.floor(height / (infoRowHeight + 4));
|
||||
|
||||
return (
|
||||
<div className={styles.infos}>
|
||||
{
|
||||
!!nextAiring &&
|
||||
<ArtistIndexOverviewInfoRow
|
||||
title={formatDateTime(nextAiring, longDateFormat, timeFormat)}
|
||||
iconName={icons.SCHEDULED}
|
||||
label={getRelativeDate(
|
||||
nextAiring,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true
|
||||
}
|
||||
)}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
rows.map((row) => {
|
||||
if (!isVisible(row, props)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (shownRows >= maxRows) {
|
||||
return null;
|
||||
}
|
||||
|
||||
shownRows++;
|
||||
|
||||
const infoRowProps = getInfoRowProps(row, props);
|
||||
|
||||
return (
|
||||
<ArtistIndexOverviewInfoRow
|
||||
key={row.name}
|
||||
{...infoRowProps}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ArtistIndexOverviewInfo.propTypes = {
|
||||
height: PropTypes.number.isRequired,
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
showAdded: PropTypes.bool.isRequired,
|
||||
showAlbumCount: PropTypes.bool.isRequired,
|
||||
showPath: PropTypes.bool.isRequired,
|
||||
showSizeOnDisk: PropTypes.bool.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
nextAiring: PropTypes.string,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
lastAlbum: PropTypes.object,
|
||||
added: PropTypes.string,
|
||||
albumCount: PropTypes.number.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
sizeOnDisk: PropTypes.number,
|
||||
sortKey: PropTypes.string.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexOverviewInfo;
|
@ -0,0 +1,228 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Album from 'Album/Album';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import ArtistIndexOverviewInfoRow from './ArtistIndexOverviewInfoRow';
|
||||
import styles from './ArtistIndexOverviewInfo.css';
|
||||
|
||||
const infoRowHeight = parseInt(dimensions.artistIndexOverviewInfoRowHeight);
|
||||
|
||||
const rows = [
|
||||
{
|
||||
name: 'monitored',
|
||||
showProp: 'showMonitored',
|
||||
valueProp: 'monitored',
|
||||
},
|
||||
{
|
||||
name: 'qualityProfileId',
|
||||
showProp: 'showQualityProfile',
|
||||
valueProp: 'qualityProfileId',
|
||||
},
|
||||
{
|
||||
name: 'lastAlbum',
|
||||
showProp: 'showLastAlbum',
|
||||
valueProp: 'lastAlbum',
|
||||
},
|
||||
{
|
||||
name: 'added',
|
||||
showProp: 'showAdded',
|
||||
valueProp: 'added',
|
||||
},
|
||||
{
|
||||
name: 'albumCount',
|
||||
showProp: 'showAlbumCount',
|
||||
valueProp: 'albumCount',
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
showProp: 'showPath',
|
||||
valueProp: 'path',
|
||||
},
|
||||
{
|
||||
name: 'sizeOnDisk',
|
||||
showProp: 'showSizeOnDisk',
|
||||
valueProp: 'sizeOnDisk',
|
||||
},
|
||||
];
|
||||
|
||||
function getInfoRowProps(row, props, uiSettings) {
|
||||
const { name } = row;
|
||||
|
||||
if (name === 'monitored') {
|
||||
const monitoredText = props.monitored ? 'Monitored' : 'Unmonitored';
|
||||
|
||||
return {
|
||||
title: monitoredText,
|
||||
iconName: props.monitored ? icons.MONITORED : icons.UNMONITORED,
|
||||
label: monitoredText,
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
return {
|
||||
title: 'Quality Profile',
|
||||
iconName: icons.PROFILE,
|
||||
label: props.qualityProfile.name,
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'lastAlbum' && !!props.lastAlbum?.title) {
|
||||
const lastAlbum = props.lastAlbum;
|
||||
const { showRelativeDates, shortDateFormat, timeFormat } = uiSettings;
|
||||
|
||||
return {
|
||||
title: `Last Album: ${lastAlbum.title}`,
|
||||
iconName: icons.CALENDAR,
|
||||
label: getRelativeDate(
|
||||
lastAlbum.releaseDate,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true,
|
||||
}
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
const added = props.added;
|
||||
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
|
||||
uiSettings;
|
||||
|
||||
return {
|
||||
title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`,
|
||||
iconName: icons.ADD,
|
||||
label: getRelativeDate(added, shortDateFormat, showRelativeDates, {
|
||||
timeFormat,
|
||||
timeForToday: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'albumCount') {
|
||||
const { albumCount } = props;
|
||||
let albums = '1 album';
|
||||
|
||||
if (albumCount === 0) {
|
||||
albums = 'No albums';
|
||||
} else if (albumCount > 1) {
|
||||
albums = `${albumCount} albums`;
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Album Count',
|
||||
iconName: icons.CIRCLE,
|
||||
label: albums,
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'path') {
|
||||
return {
|
||||
title: 'Path',
|
||||
iconName: icons.FOLDER,
|
||||
label: props.path,
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'sizeOnDisk') {
|
||||
return {
|
||||
title: 'Size on Disk',
|
||||
iconName: icons.DRIVE,
|
||||
label: formatBytes(props.sizeOnDisk),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface ArtistIndexOverviewInfoProps {
|
||||
height: number;
|
||||
showMonitored: boolean;
|
||||
showQualityProfile: boolean;
|
||||
showLastAlbum: boolean;
|
||||
showAdded: boolean;
|
||||
showAlbumCount: boolean;
|
||||
showPath: boolean;
|
||||
showSizeOnDisk: boolean;
|
||||
monitored: boolean;
|
||||
nextAlbum?: Album;
|
||||
qualityProfile: object;
|
||||
lastAlbum?: Album;
|
||||
added?: string;
|
||||
albumCount: number;
|
||||
path: string;
|
||||
sizeOnDisk?: number;
|
||||
sortKey: string;
|
||||
}
|
||||
|
||||
function ArtistIndexOverviewInfo(props: ArtistIndexOverviewInfoProps) {
|
||||
const { height, nextAlbum } = props;
|
||||
|
||||
const uiSettings = useSelector(createUISettingsSelector());
|
||||
|
||||
const { shortDateFormat, showRelativeDates, longDateFormat, timeFormat } =
|
||||
uiSettings;
|
||||
|
||||
let shownRows = 1;
|
||||
const maxRows = Math.floor(height / (infoRowHeight + 4));
|
||||
|
||||
const rowInfo = useMemo(() => {
|
||||
return rows.map((row) => {
|
||||
const { name, showProp, valueProp } = row;
|
||||
|
||||
const isVisible =
|
||||
props[valueProp] != null && (props[showProp] || props.sortKey === name);
|
||||
|
||||
return {
|
||||
...row,
|
||||
isVisible,
|
||||
};
|
||||
});
|
||||
}, [props]);
|
||||
|
||||
return (
|
||||
<div className={styles.infos}>
|
||||
{!!nextAlbum?.releaseDate && (
|
||||
<ArtistIndexOverviewInfoRow
|
||||
title={formatDateTime(
|
||||
nextAlbum.releaseDate,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
)}
|
||||
iconName={icons.SCHEDULED}
|
||||
label={getRelativeDate(
|
||||
nextAlbum.releaseDate,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true,
|
||||
}
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{rowInfo.map((row) => {
|
||||
if (!row.isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (shownRows >= maxRows) {
|
||||
return null;
|
||||
}
|
||||
|
||||
shownRows++;
|
||||
|
||||
const infoRowProps = getInfoRowProps(row, props, uiSettings);
|
||||
|
||||
return <ArtistIndexOverviewInfoRow key={row.name} {...infoRowProps} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexOverviewInfo;
|
@ -1,35 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import styles from './ArtistIndexOverviewInfoRow.css';
|
||||
|
||||
function ArtistIndexOverviewInfoRow(props) {
|
||||
const {
|
||||
title,
|
||||
iconName,
|
||||
label
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.infoRow}
|
||||
title={title}
|
||||
>
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
name={iconName}
|
||||
size={14}
|
||||
/>
|
||||
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ArtistIndexOverviewInfoRow.propTypes = {
|
||||
title: PropTypes.string,
|
||||
iconName: PropTypes.object.isRequired,
|
||||
label: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexOverviewInfoRow;
|
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import styles from './ArtistIndexOverviewInfoRow.css';
|
||||
|
||||
interface ArtistIndexOverviewInfoRowProps {
|
||||
title?: string;
|
||||
iconName: object;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function ArtistIndexOverviewInfoRow(props: ArtistIndexOverviewInfoRowProps) {
|
||||
const { title, iconName, label } = props;
|
||||
|
||||
return (
|
||||
<div className={styles.infoRow} title={title}>
|
||||
<Icon className={styles.icon} name={iconName} size={14} />
|
||||
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexOverviewInfoRow;
|
@ -1,275 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Grid, WindowScroller } from 'react-virtualized';
|
||||
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
||||
import Measure from 'Components/Measure';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import ArtistIndexOverview from './ArtistIndexOverview';
|
||||
import styles from './ArtistIndexOverviews.css';
|
||||
|
||||
// Poster container dimensions
|
||||
const columnPadding = parseInt(dimensions.artistIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen);
|
||||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||
|
||||
function calculatePosterWidth(posterSize, isSmallScreen) {
|
||||
const maxiumPosterWidth = isSmallScreen ? 192 : 202;
|
||||
|
||||
if (posterSize === 'large') {
|
||||
return maxiumPosterWidth;
|
||||
}
|
||||
|
||||
if (posterSize === 'medium') {
|
||||
return Math.floor(maxiumPosterWidth * 0.75);
|
||||
}
|
||||
|
||||
return Math.floor(maxiumPosterWidth * 0.5);
|
||||
}
|
||||
|
||||
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
|
||||
const {
|
||||
detailedProgressBar
|
||||
} = overviewOptions;
|
||||
|
||||
const heights = [
|
||||
posterHeight,
|
||||
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
|
||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||
];
|
||||
|
||||
return heights.reduce((acc, height) => acc + height, 0);
|
||||
}
|
||||
|
||||
function calculatePosterHeight(posterWidth) {
|
||||
return posterWidth;
|
||||
}
|
||||
|
||||
class ArtistIndexOverviews extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
width: 0,
|
||||
columnCount: 1,
|
||||
posterWidth: 238,
|
||||
posterHeight: 238,
|
||||
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}),
|
||||
scrollRestored: false
|
||||
};
|
||||
|
||||
this._grid = null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
overviewOptions,
|
||||
jumpToCharacter,
|
||||
scrollTop,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
rowHeight,
|
||||
scrollRestored
|
||||
} = this.state;
|
||||
|
||||
if (prevProps.sortKey !== sortKey ||
|
||||
prevProps.overviewOptions !== overviewOptions) {
|
||||
this.calculateGrid(this.state.width, isSmallScreen);
|
||||
}
|
||||
|
||||
if (
|
||||
this._grid &&
|
||||
(prevState.width !== width ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items) ||
|
||||
prevProps.overviewOptions !== overviewOptions
|
||||
)
|
||||
) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
||||
this.setState({ scrollRestored: true });
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
|
||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (this._grid && index != null) {
|
||||
|
||||
this._grid.scrollToCell({
|
||||
rowIndex: index,
|
||||
columnIndex: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setGridRef = (ref) => {
|
||||
this._grid = ref;
|
||||
};
|
||||
|
||||
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||
const {
|
||||
sortKey,
|
||||
overviewOptions
|
||||
} = this.props;
|
||||
|
||||
const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen);
|
||||
const posterHeight = calculatePosterHeight(posterWidth);
|
||||
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions);
|
||||
|
||||
this.setState({
|
||||
width,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
rowHeight
|
||||
});
|
||||
};
|
||||
|
||||
cellRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
overviewOptions,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
rowHeight
|
||||
} = this.state;
|
||||
|
||||
const artist = items[rowIndex];
|
||||
|
||||
if (!artist) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
style={style}
|
||||
>
|
||||
<ArtistIndexItemConnector
|
||||
key={artist.id}
|
||||
component={ArtistIndexOverview}
|
||||
sortKey={sortKey}
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
rowHeight={rowHeight}
|
||||
overviewOptions={overviewOptions}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
longDateFormat={longDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
isSmallScreen={isSmallScreen}
|
||||
artistId={artist.id}
|
||||
qualityProfileId={artist.qualityProfileId}
|
||||
metadataProfileId={artist.metadataProfileId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMeasure = ({ width }) => {
|
||||
this.calculateGrid(width, this.props.isSmallScreen);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
isSmallScreen,
|
||||
scroller
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
rowHeight
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Measure
|
||||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<WindowScroller
|
||||
scrollElement={isSmallScreen ? undefined : scroller}
|
||||
>
|
||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||
if (!height) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={registerChild}>
|
||||
<Grid
|
||||
ref={this.setGridRef}
|
||||
className={styles.grid}
|
||||
autoHeight={true}
|
||||
height={height}
|
||||
columnCount={1}
|
||||
columnWidth={width}
|
||||
rowCount={items.length}
|
||||
rowHeight={rowHeight}
|
||||
width={width}
|
||||
onScroll={onChildScroll}
|
||||
scrollTop={scrollTop}
|
||||
overscanRowCount={2}
|
||||
cellRenderer={this.cellRenderer}
|
||||
onSectionRendered={this.onSectionRendered}
|
||||
scrollToAlignment={'start'}
|
||||
isScrollingOptOut={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</WindowScroller>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexOverviews.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
overviewOptions: PropTypes.object.isRequired,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
jumpToCharacter: PropTypes.string,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexOverviews;
|
@ -0,0 +1,203 @@
|
||||
import { throttle } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||
import Artist from 'Artist/Artist';
|
||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
import ArtistIndexOverview from './ArtistIndexOverview';
|
||||
import selectOverviewOptions from './selectOverviewOptions';
|
||||
|
||||
// Poster container dimensions
|
||||
const columnPadding = parseInt(dimensions.artistIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(
|
||||
dimensions.artistIndexColumnPaddingSmallScreen
|
||||
);
|
||||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||
const bodyPadding = parseInt(dimensions.pageContentBodyPadding);
|
||||
const bodyPaddingSmallScreen = parseInt(
|
||||
dimensions.pageContentBodyPaddingSmallScreen
|
||||
);
|
||||
|
||||
interface RowItemData {
|
||||
items: Artist[];
|
||||
sortKey: string;
|
||||
posterWidth: number;
|
||||
posterHeight: number;
|
||||
rowHeight: number;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
interface ArtistIndexOverviewsProps {
|
||||
items: Artist[];
|
||||
sortKey?: string;
|
||||
sortDirection?: string;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
||||
index,
|
||||
style,
|
||||
data,
|
||||
}) => {
|
||||
const { items, ...otherData } = data;
|
||||
|
||||
if (index >= items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const artist = items[index];
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<ArtistIndexOverview artistId={artist.id} {...otherData} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getWindowScrollTopPosition() {
|
||||
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||
}
|
||||
|
||||
function ArtistIndexOverviews(props: ArtistIndexOverviewsProps) {
|
||||
const { items, sortKey, jumpToCharacter, isSmallScreen, scrollerRef } = props;
|
||||
|
||||
const { size: posterSize, detailedProgressBar } = useSelector(
|
||||
selectOverviewOptions
|
||||
);
|
||||
const listRef: React.MutableRefObject<List> = useRef();
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
const posterWidth = useMemo(() => {
|
||||
const maxiumPosterWidth = isSmallScreen ? 192 : 202;
|
||||
|
||||
if (posterSize === 'large') {
|
||||
return maxiumPosterWidth;
|
||||
}
|
||||
|
||||
if (posterSize === 'medium') {
|
||||
return Math.floor(maxiumPosterWidth * 0.75);
|
||||
}
|
||||
|
||||
return Math.floor(maxiumPosterWidth * 0.5);
|
||||
}, [posterSize, isSmallScreen]);
|
||||
|
||||
const posterHeight = useMemo(() => {
|
||||
return posterWidth;
|
||||
}, [posterWidth]);
|
||||
|
||||
const rowHeight = useMemo(() => {
|
||||
const heights = [
|
||||
posterHeight,
|
||||
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
|
||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding,
|
||||
];
|
||||
|
||||
return heights.reduce((acc, height) => acc + height, 0);
|
||||
}, [detailedProgressBar, posterHeight, isSmallScreen]);
|
||||
|
||||
useEffect(() => {
|
||||
const current = scrollerRef.current as HTMLElement;
|
||||
|
||||
if (isSmallScreen) {
|
||||
setSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (current) {
|
||||
const width = current.clientWidth;
|
||||
const padding =
|
||||
(isSmallScreen ? bodyPaddingSmallScreen : bodyPadding) - 5;
|
||||
|
||||
setSize({
|
||||
width: width - padding * 2,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
}, [isSmallScreen, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
const scrollTop =
|
||||
(isSmallScreen
|
||||
? getWindowScrollTopPosition()
|
||||
: currentScrollerRef.scrollTop) - offsetTop;
|
||||
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
}, 10);
|
||||
|
||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
handleScroll.cancel();
|
||||
|
||||
if (currentScrollListener) {
|
||||
currentScrollListener.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
}, [isSmallScreen, listRef, scrollerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (jumpToCharacter) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (index != null) {
|
||||
let scrollTop = index * rowHeight;
|
||||
|
||||
// If the offset is zero go to the top, otherwise offset
|
||||
// by the approximate size of the header + padding (37 + 20).
|
||||
if (scrollTop > 0) {
|
||||
const offset = 57;
|
||||
|
||||
scrollTop += offset;
|
||||
}
|
||||
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
scrollerRef.current.scrollTo(0, scrollTop);
|
||||
}
|
||||
}
|
||||
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
||||
|
||||
return (
|
||||
<div ref={measureRef}>
|
||||
<List<RowItemData>
|
||||
ref={listRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'none',
|
||||
}}
|
||||
width={size.width}
|
||||
height={size.height}
|
||||
itemCount={items.length}
|
||||
itemSize={rowHeight}
|
||||
itemData={{
|
||||
items,
|
||||
sortKey,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
rowHeight,
|
||||
isSmallScreen,
|
||||
}}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexOverviews;
|
@ -1,25 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import ArtistIndexOverviews from './ArtistIndexOverviews';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.artistIndex.overviewOptions,
|
||||
createUISettingsSelector(),
|
||||
createDimensionsSelector(),
|
||||
(overviewOptions, uiSettings, dimensions) => {
|
||||
return {
|
||||
overviewOptions,
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(ArtistIndexOverviews);
|
@ -1,25 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ArtistIndexOverviewOptionsModalContentConnector from './ArtistIndexOverviewOptionsModalContentConnector';
|
||||
|
||||
function ArtistIndexOverviewOptionsModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ArtistIndexOverviewOptionsModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ArtistIndexOverviewOptionsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexOverviewOptionsModal;
|
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ArtistIndexOverviewOptionsModalContent from './ArtistIndexOverviewOptionsModalContent';
|
||||
|
||||
interface ArtistIndexOverviewOptionsModalProps {
|
||||
isOpen: boolean;
|
||||
onModalClose(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
function ArtistIndexOverviewOptionsModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
}: ArtistIndexOverviewOptionsModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<ArtistIndexOverviewOptionsModalContent
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexOverviewOptionsModal;
|
@ -1,308 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const posterSizeOptions = [
|
||||
{ key: 'small', value: 'Small' },
|
||||
{ key: 'medium', value: 'Medium' },
|
||||
{ key: 'large', value: 'Large' }
|
||||
];
|
||||
|
||||
class ArtistIndexOverviewOptionsModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
detailedProgressBar: props.detailedProgressBar,
|
||||
size: props.size,
|
||||
showMonitored: props.showMonitored,
|
||||
showQualityProfile: props.showQualityProfile,
|
||||
showLastAlbum: props.showLastAlbum,
|
||||
showAdded: props.showAdded,
|
||||
showAlbumCount: props.showAlbumCount,
|
||||
showPath: props.showPath,
|
||||
showSizeOnDisk: props.showSizeOnDisk,
|
||||
showSearchAction: props.showSearchAction
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
detailedProgressBar,
|
||||
size,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showLastAlbum,
|
||||
showAdded,
|
||||
showAlbumCount,
|
||||
showPath,
|
||||
showSizeOnDisk,
|
||||
showSearchAction
|
||||
} = this.props;
|
||||
|
||||
const state = {};
|
||||
|
||||
if (detailedProgressBar !== prevProps.detailedProgressBar) {
|
||||
state.detailedProgressBar = detailedProgressBar;
|
||||
}
|
||||
|
||||
if (size !== prevProps.size) {
|
||||
state.size = size;
|
||||
}
|
||||
|
||||
if (showMonitored !== prevProps.showMonitored) {
|
||||
state.showMonitored = showMonitored;
|
||||
}
|
||||
|
||||
if (showQualityProfile !== prevProps.showQualityProfile) {
|
||||
state.showQualityProfile = showQualityProfile;
|
||||
}
|
||||
|
||||
if (showLastAlbum !== prevProps.showLastAlbum) {
|
||||
state.showLastAlbum = showLastAlbum;
|
||||
}
|
||||
|
||||
if (showAdded !== prevProps.showAdded) {
|
||||
state.showAdded = showAdded;
|
||||
}
|
||||
|
||||
if (showAlbumCount !== prevProps.showAlbumCount) {
|
||||
state.showAlbumCount = showAlbumCount;
|
||||
}
|
||||
|
||||
if (showPath !== prevProps.showPath) {
|
||||
state.showPath = showPath;
|
||||
}
|
||||
|
||||
if (showSizeOnDisk !== prevProps.showSizeOnDisk) {
|
||||
state.showSizeOnDisk = showSizeOnDisk;
|
||||
}
|
||||
|
||||
if (showSearchAction !== prevProps.showSearchAction) {
|
||||
state.showSearchAction = showSearchAction;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(state)) {
|
||||
this.setState(state);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChangeOverviewOption = ({ name, value }) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
}, () => {
|
||||
this.props.onChangeOverviewOption({ [name]: value });
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
detailedProgressBar,
|
||||
size,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showLastAlbum,
|
||||
showAdded,
|
||||
showAlbumCount,
|
||||
showPath,
|
||||
showSizeOnDisk,
|
||||
showSearchAction
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Overview Options
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('PosterSize')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="size"
|
||||
value={size}
|
||||
values={posterSizeOptions}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('DetailedProgressBar')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="detailedProgressBar"
|
||||
value={detailedProgressBar}
|
||||
helpText={translate('DetailedProgressBarHelpText')}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowMonitored')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showMonitored"
|
||||
value={showMonitored}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
|
||||
<FormLabel>
|
||||
{translate('ShowQualityProfile')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showQualityProfile"
|
||||
value={showQualityProfile}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowLastAlbum')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showLastAlbum"
|
||||
value={showLastAlbum}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowDateAdded')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showAdded"
|
||||
value={showAdded}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowAlbumCount')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showAlbumCount"
|
||||
value={showAlbumCount}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowPath')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showPath"
|
||||
value={showPath}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowSizeOnDisk')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showSizeOnDisk"
|
||||
value={showSizeOnDisk}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowSearch')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showSearchAction"
|
||||
value={showSearchAction}
|
||||
helpText={translate('ShowSearchActionHelpText')}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexOverviewOptionsModalContent.propTypes = {
|
||||
size: PropTypes.string.isRequired,
|
||||
detailedProgressBar: PropTypes.bool.isRequired,
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
showLastAlbum: PropTypes.bool.isRequired,
|
||||
showAdded: PropTypes.bool.isRequired,
|
||||
showAlbumCount: PropTypes.bool.isRequired,
|
||||
showPath: PropTypes.bool.isRequired,
|
||||
showSizeOnDisk: PropTypes.bool.isRequired,
|
||||
showSearchAction: PropTypes.bool.isRequired,
|
||||
onChangeOverviewOption: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexOverviewOptionsModalContent;
|
@ -0,0 +1,197 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { setArtistOverviewOption } from 'Store/Actions/artistIndexActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import selectOverviewOptions from '../selectOverviewOptions';
|
||||
|
||||
const posterSizeOptions = [
|
||||
{
|
||||
key: 'small',
|
||||
get value() {
|
||||
return translate('Small');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'medium',
|
||||
get value() {
|
||||
return translate('Medium');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'large',
|
||||
get value() {
|
||||
return translate('Large');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
interface ArtistIndexOverviewOptionsModalContentProps {
|
||||
onModalClose(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
function ArtistIndexOverviewOptionsModalContent(
|
||||
props: ArtistIndexOverviewOptionsModalContentProps
|
||||
) {
|
||||
const { onModalClose } = props;
|
||||
|
||||
const {
|
||||
detailedProgressBar,
|
||||
size,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showLastAlbum,
|
||||
showAdded,
|
||||
showAlbumCount,
|
||||
showPath,
|
||||
showSizeOnDisk,
|
||||
showSearchAction,
|
||||
} = useSelector(selectOverviewOptions);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onOverviewOptionChange = useCallback(
|
||||
({ name, value }) => {
|
||||
dispatch(setArtistOverviewOption({ [name]: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('OverviewOptions')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PosterSize')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="size"
|
||||
value={size}
|
||||
values={posterSizeOptions}
|
||||
onChange={onOverviewOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('DetailedProgressBar')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="detailedProgressBar"
|
||||
value={detailedProgressBar}
|
||||
helpText={translate('DetailedProgressBarHelpText')}
|
||||
onChange={onOverviewOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowMonitored')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showMonitored"
|
||||
value={showMonitored}
|
||||
onChange={onOverviewOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowQualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showQualityProfile"
|
||||
value={showQualityProfile}
|
||||
onChange={onOverviewOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowLastAlbum')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showLastAlbum"
|
||||
value={showLastAlbum}
|
||||
onChange={onOverviewOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowDateAdded')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showAdded"
|
||||
value={showAdded}
|
||||
onChange={onOverviewOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowAlbumCount')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showAlbumCount"
|
||||
value={showAlbumCount}
|
||||
onChange={onOverviewOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowPath')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showPath"
|
||||
value={showPath}
|
||||
onChange={onOverviewOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowSizeOnDisk')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showSizeOnDisk"
|
||||
value={showSizeOnDisk}
|
||||
onChange={onOverviewOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showSearchAction"
|
||||
value={showSearchAction}
|
||||
helpText={translate('ShowSearchActionHelpText')}
|
||||
onChange={onOverviewOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexOverviewOptionsModalContent;
|
@ -1,23 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setArtistOverviewOption } from 'Store/Actions/artistIndexActions';
|
||||
import ArtistIndexOverviewOptionsModalContent from './ArtistIndexOverviewOptionsModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.artistIndex,
|
||||
(artistIndex) => {
|
||||
return artistIndex.overviewOptions;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onChangeOverviewOption(payload) {
|
||||
dispatch(setArtistOverviewOption(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexOverviewOptionsModalContent);
|
@ -0,0 +1,9 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
const selectOverviewOptions = createSelector(
|
||||
(state: AppState) => state.artistIndex.overviewOptions,
|
||||
(overviewOptions) => overviewOptions
|
||||
);
|
||||
|
||||
export default selectOverviewOptions;
|
@ -1,305 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ArtistPoster from 'Artist/ArtistPoster';
|
||||
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||
import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ArtistIndexPosterInfo from './ArtistIndexPosterInfo';
|
||||
import styles from './ArtistIndexPoster.css';
|
||||
|
||||
class ArtistIndexPoster extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
hasPosterError: false,
|
||||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditArtistPress = () => {
|
||||
this.setState({ isEditArtistModalOpen: true });
|
||||
};
|
||||
|
||||
onEditArtistModalClose = () => {
|
||||
this.setState({ isEditArtistModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteArtistPress = () => {
|
||||
this.setState({
|
||||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteArtistModalClose = () => {
|
||||
this.setState({ isDeleteArtistModalOpen: false });
|
||||
};
|
||||
|
||||
onPosterLoad = () => {
|
||||
if (this.state.hasPosterError) {
|
||||
this.setState({ hasPosterError: false });
|
||||
}
|
||||
};
|
||||
|
||||
onPosterLoadError = () => {
|
||||
if (!this.state.hasPosterError) {
|
||||
this.setState({ hasPosterError: true });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
artistName,
|
||||
monitored,
|
||||
foreignArtistId,
|
||||
status,
|
||||
nextAlbum,
|
||||
lastAlbum,
|
||||
statistics,
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
qualityProfile,
|
||||
showNextAlbum,
|
||||
showSearchAction,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
isRefreshingArtist,
|
||||
isSearchingArtist,
|
||||
onRefreshArtistPress,
|
||||
onSearchPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
albumCount,
|
||||
sizeOnDisk,
|
||||
trackCount,
|
||||
trackFileCount,
|
||||
totalTrackCount
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
hasPosterError,
|
||||
isEditArtistModalOpen,
|
||||
isDeleteArtistModalOpen
|
||||
} = this.state;
|
||||
|
||||
const link = `/artist/${foreignArtistId}`;
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}px`
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.posterContainer} title={artistName}>
|
||||
<Label className={styles.controls}>
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
name={icons.REFRESH}
|
||||
title={translate('RefreshArtist')}
|
||||
isSpinning={isRefreshingArtist}
|
||||
onPress={onRefreshArtistPress}
|
||||
/>
|
||||
|
||||
{
|
||||
showSearchAction &&
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
name={icons.SEARCH}
|
||||
title={translate('SearchForMonitoredAlbums')}
|
||||
isSpinning={isSearchingArtist}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
}
|
||||
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.EDIT}
|
||||
title={translate('EditArtist')}
|
||||
onPress={this.onEditArtistPress}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{
|
||||
status === 'ended' &&
|
||||
<div
|
||||
className={styles.ended}
|
||||
title={translate('Inactive')}
|
||||
/>
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
style={elementStyle}
|
||||
to={link}
|
||||
>
|
||||
<ArtistPoster
|
||||
className={styles.poster}
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
onError={this.onPosterLoadError}
|
||||
onLoad={this.onPosterLoad}
|
||||
/>
|
||||
|
||||
{
|
||||
hasPosterError &&
|
||||
<div className={styles.overlayTitle}>
|
||||
{artistName}
|
||||
</div>
|
||||
}
|
||||
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ArtistIndexProgressBar
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
trackCount={trackCount}
|
||||
trackFileCount={trackFileCount}
|
||||
totalTrackCount={totalTrackCount}
|
||||
posterWidth={posterWidth}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
/>
|
||||
|
||||
{
|
||||
showTitle &&
|
||||
<div className={styles.title} title={artistName}>
|
||||
{artistName}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showMonitored &&
|
||||
<div className={styles.title}>
|
||||
{monitored ? 'Monitored' : 'Unmonitored'}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showQualityProfile &&
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showNextAlbum && !!nextAlbum?.releaseDate &&
|
||||
<div className={styles.nextAlbum} title={translate('NextAlbum')}>
|
||||
{
|
||||
getRelativeDate(
|
||||
nextAlbum.releaseDate,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true
|
||||
}
|
||||
)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<ArtistIndexPosterInfo
|
||||
nextAlbum={nextAlbum}
|
||||
lastAlbum={lastAlbum}
|
||||
albumCount={albumCount}
|
||||
sizeOnDisk={sizeOnDisk}
|
||||
qualityProfile={qualityProfile}
|
||||
showQualityProfile={showQualityProfile}
|
||||
showNextAlbum={showNextAlbum}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
longDateFormat={longDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
<EditArtistModalConnector
|
||||
isOpen={isEditArtistModalOpen}
|
||||
artistId={id}
|
||||
onModalClose={this.onEditArtistModalClose}
|
||||
onDeleteArtistPress={this.onDeleteArtistPress}
|
||||
/>
|
||||
|
||||
<DeleteArtistModal
|
||||
isOpen={isDeleteArtistModalOpen}
|
||||
artistId={id}
|
||||
onModalClose={this.onDeleteArtistModalClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexPoster.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
artistName: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
nextAlbum: PropTypes.object,
|
||||
lastAlbum: PropTypes.object,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
posterWidth: PropTypes.number.isRequired,
|
||||
posterHeight: PropTypes.number.isRequired,
|
||||
detailedProgressBar: PropTypes.bool.isRequired,
|
||||
showTitle: PropTypes.bool.isRequired,
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
showNextAlbum: PropTypes.bool.isRequired,
|
||||
showSearchAction: PropTypes.bool.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
isRefreshingArtist: PropTypes.bool.isRequired,
|
||||
isSearchingArtist: PropTypes.bool.isRequired,
|
||||
onRefreshArtistPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ArtistIndexPoster.defaultProps = {
|
||||
statistics: {
|
||||
albumCount: 0,
|
||||
trackCount: 0,
|
||||
trackFileCount: 0,
|
||||
totalTrackCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
export default ArtistIndexPoster;
|
@ -0,0 +1,257 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Statistics } from 'Artist/Artist';
|
||||
import ArtistPoster from 'Artist/ArtistPoster';
|
||||
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||
import createArtistIndexItemSelector from 'Artist/Index/createArtistIndexItemSelector';
|
||||
import ArtistIndexPosterInfo from 'Artist/Index/Posters/ArtistIndexPosterInfo';
|
||||
import ArtistIndexProgressBar from 'Artist/Index/ProgressBar/ArtistIndexProgressBar';
|
||||
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import selectPosterOptions from './selectPosterOptions';
|
||||
import styles from './ArtistIndexPoster.css';
|
||||
|
||||
interface ArtistIndexPosterProps {
|
||||
artistId: number;
|
||||
sortKey: string;
|
||||
posterWidth: number;
|
||||
posterHeight: number;
|
||||
}
|
||||
|
||||
function ArtistIndexPoster(props: ArtistIndexPosterProps) {
|
||||
const { artistId, sortKey, posterWidth, posterHeight } = props;
|
||||
|
||||
const {
|
||||
artist,
|
||||
qualityProfile,
|
||||
metadataProfile,
|
||||
isRefreshingArtist,
|
||||
isSearchingArtist,
|
||||
} = useSelector(createArtistIndexItemSelector(props.artistId));
|
||||
|
||||
const {
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showNextAlbum,
|
||||
showSearchAction,
|
||||
} = useSelector(selectPosterOptions);
|
||||
|
||||
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
|
||||
useSelector(createUISettingsSelector());
|
||||
|
||||
const {
|
||||
artistName,
|
||||
artistType,
|
||||
monitored,
|
||||
status,
|
||||
path,
|
||||
foreignArtistId,
|
||||
nextAlbum,
|
||||
added,
|
||||
statistics = {} as Statistics,
|
||||
images,
|
||||
tags,
|
||||
} = artist;
|
||||
|
||||
const {
|
||||
albumCount = 0,
|
||||
trackCount = 0,
|
||||
trackFileCount = 0,
|
||||
totalTrackCount = 0,
|
||||
sizeOnDisk = 0,
|
||||
} = statistics;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [hasPosterError, setHasPosterError] = useState(false);
|
||||
const [isEditArtistModalOpen, setIsEditArtistModalOpen] = useState(false);
|
||||
const [isDeleteArtistModalOpen, setIsDeleteArtistModalOpen] = useState(false);
|
||||
|
||||
const onRefreshPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: REFRESH_ARTIST,
|
||||
artistId,
|
||||
})
|
||||
);
|
||||
}, [artistId, dispatch]);
|
||||
|
||||
const onSearchPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: ARTIST_SEARCH,
|
||||
artistId,
|
||||
})
|
||||
);
|
||||
}, [artistId, dispatch]);
|
||||
|
||||
const onPosterLoadError = useCallback(() => {
|
||||
setHasPosterError(true);
|
||||
}, [setHasPosterError]);
|
||||
|
||||
const onPosterLoad = useCallback(() => {
|
||||
setHasPosterError(false);
|
||||
}, [setHasPosterError]);
|
||||
|
||||
const onEditArtistPress = useCallback(() => {
|
||||
setIsEditArtistModalOpen(true);
|
||||
}, [setIsEditArtistModalOpen]);
|
||||
|
||||
const onEditArtistModalClose = useCallback(() => {
|
||||
setIsEditArtistModalOpen(false);
|
||||
}, [setIsEditArtistModalOpen]);
|
||||
|
||||
const onDeleteArtistPress = useCallback(() => {
|
||||
setIsEditArtistModalOpen(false);
|
||||
setIsDeleteArtistModalOpen(true);
|
||||
}, [setIsDeleteArtistModalOpen]);
|
||||
|
||||
const onDeleteArtistModalClose = useCallback(() => {
|
||||
setIsDeleteArtistModalOpen(false);
|
||||
}, [setIsDeleteArtistModalOpen]);
|
||||
|
||||
const link = `/artist/${foreignArtistId}`;
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}px`,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<div className={styles.posterContainer}>
|
||||
<Label className={styles.controls}>
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
name={icons.REFRESH}
|
||||
itle={translate('RefreshArtist')}
|
||||
isSpinning={isRefreshingArtist}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
|
||||
{showSearchAction ? (
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
name={icons.SEARCH}
|
||||
title={translate('SearchForMonitoredAlbums')}
|
||||
isSpinning={isSearchingArtist}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<IconButton
|
||||
className={styles.action}
|
||||
name={icons.EDIT}
|
||||
title={translate('EditArtist')}
|
||||
onPress={onEditArtistPress}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{status === 'ended' ? (
|
||||
<div className={styles.ended} title={translate('Inactive')} />
|
||||
) : null}
|
||||
|
||||
<Link className={styles.link} style={elementStyle} to={link}>
|
||||
<ArtistPoster
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
onError={onPosterLoadError}
|
||||
onLoad={onPosterLoad}
|
||||
/>
|
||||
|
||||
{hasPosterError ? (
|
||||
<div className={styles.overlayTitle}>{artistName}</div>
|
||||
) : null}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ArtistIndexProgressBar
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
trackCount={trackCount}
|
||||
trackFileCount={trackFileCount}
|
||||
totalTrackCount={totalTrackCount}
|
||||
posterWidth={posterWidth}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
/>
|
||||
|
||||
{showTitle ? (
|
||||
<div className={styles.title} title={artistName}>
|
||||
{artistName}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showMonitored ? (
|
||||
<div className={styles.title}>
|
||||
{monitored ? translate('Monitored') : translate('Unmonitored')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showQualityProfile ? (
|
||||
<div className={styles.title} title={translate('QualityProfile')}>
|
||||
{qualityProfile.name}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{showNextAlbum && !!nextAlbum?.releaseDate ? (
|
||||
<div className={styles.nextAlbum} title={translate('NextAlbum')}>
|
||||
{getRelativeDate(
|
||||
nextAlbum.releaseDate,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ArtistIndexPosterInfo
|
||||
artistType={artistType}
|
||||
added={added}
|
||||
albumCount={albumCount}
|
||||
sizeOnDisk={sizeOnDisk}
|
||||
path={path}
|
||||
tags={tags}
|
||||
qualityProfile={qualityProfile}
|
||||
metadataProfile={metadataProfile}
|
||||
showQualityProfile={showQualityProfile}
|
||||
showNextAlbum={showNextAlbum}
|
||||
showRelativeDates={showRelativeDates}
|
||||
sortKey={sortKey}
|
||||
shortDateFormat={shortDateFormat}
|
||||
longDateFormat={longDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
/>
|
||||
|
||||
<EditArtistModalConnector
|
||||
isOpen={isEditArtistModalOpen}
|
||||
artistId={artistId}
|
||||
onModalClose={onEditArtistModalClose}
|
||||
onDeleteArtistPress={onDeleteArtistPress}
|
||||
/>
|
||||
|
||||
<DeleteArtistModal
|
||||
isOpen={isDeleteArtistModalOpen}
|
||||
artistId={artistId}
|
||||
onModalClose={onDeleteArtistModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexPoster;
|
@ -1,351 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Grid, WindowScroller } from 'react-virtualized';
|
||||
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
||||
import Measure from 'Components/Measure';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import ArtistIndexPoster from './ArtistIndexPoster';
|
||||
import styles from './ArtistIndexPosters.css';
|
||||
|
||||
// Poster container dimensions
|
||||
const columnPadding = parseInt(dimensions.artistIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(dimensions.artistIndexColumnPaddingSmallScreen);
|
||||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||
|
||||
const additionalColumnCount = {
|
||||
small: 3,
|
||||
medium: 2,
|
||||
large: 1
|
||||
};
|
||||
|
||||
function calculateColumnWidth(width, posterSize, isSmallScreen) {
|
||||
const maxiumColumnWidth = isSmallScreen ? 172 : 182;
|
||||
const columns = Math.floor(width / maxiumColumnWidth);
|
||||
const remainder = width % maxiumColumnWidth;
|
||||
|
||||
if (remainder === 0 && posterSize === 'large') {
|
||||
return maxiumColumnWidth;
|
||||
}
|
||||
|
||||
return Math.floor(width / (columns + additionalColumnCount[posterSize]));
|
||||
}
|
||||
|
||||
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions) {
|
||||
const {
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showNextAlbum
|
||||
} = posterOptions;
|
||||
|
||||
const nextAiringHeight = 19;
|
||||
|
||||
const heights = [
|
||||
posterHeight,
|
||||
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
|
||||
nextAiringHeight,
|
||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||
];
|
||||
|
||||
if (showTitle) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showMonitored) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showQualityProfile) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showNextAlbum) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
switch (sortKey) {
|
||||
case 'artistType':
|
||||
case 'lastAlbum':
|
||||
case 'seasons':
|
||||
case 'added':
|
||||
case 'albumCount':
|
||||
case 'path':
|
||||
case 'sizeOnDisk':
|
||||
case 'tags':
|
||||
heights.push(19);
|
||||
break;
|
||||
case 'qualityProfileId':
|
||||
if (!showQualityProfile) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
case 'nextAlbum':
|
||||
if (!showNextAlbum) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// No need to add a height of 0
|
||||
}
|
||||
|
||||
return heights.reduce((acc, height) => acc + height, 0);
|
||||
}
|
||||
|
||||
function calculatePosterHeight(posterWidth) {
|
||||
return Math.ceil(posterWidth);
|
||||
}
|
||||
|
||||
class ArtistIndexPosters extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
width: 0,
|
||||
columnWidth: 182,
|
||||
columnCount: 1,
|
||||
posterWidth: 238,
|
||||
posterHeight: 238,
|
||||
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {}),
|
||||
scrollRestored: false
|
||||
};
|
||||
|
||||
this._isInitialized = false;
|
||||
this._grid = null;
|
||||
this._padding = props.isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
posterOptions,
|
||||
jumpToCharacter,
|
||||
scrollTop,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
columnWidth,
|
||||
columnCount,
|
||||
rowHeight,
|
||||
scrollRestored
|
||||
} = this.state;
|
||||
|
||||
if (prevProps.sortKey !== sortKey ||
|
||||
prevProps.posterOptions !== posterOptions) {
|
||||
this.calculateGrid(width, isSmallScreen);
|
||||
}
|
||||
|
||||
if (this._grid &&
|
||||
(prevState.width !== width ||
|
||||
prevState.columnWidth !== columnWidth ||
|
||||
prevState.columnCount !== columnCount ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items))) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
|
||||
if (this._grid && scrollTop !== 0 && !scrollRestored) {
|
||||
this.setState({ scrollRestored: true });
|
||||
this._grid.scrollToPosition({ scrollTop });
|
||||
}
|
||||
|
||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (this._grid && index != null) {
|
||||
const row = Math.floor(index / columnCount);
|
||||
|
||||
this._grid.scrollToCell({
|
||||
rowIndex: row,
|
||||
columnIndex: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setGridRef = (ref) => {
|
||||
this._grid = ref;
|
||||
};
|
||||
|
||||
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||
const {
|
||||
sortKey,
|
||||
posterOptions
|
||||
} = this.props;
|
||||
|
||||
const columnWidth = calculateColumnWidth(width, posterOptions.size, isSmallScreen);
|
||||
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
|
||||
const posterWidth = columnWidth - this._padding * 2;
|
||||
const posterHeight = calculatePosterHeight(posterWidth);
|
||||
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions);
|
||||
|
||||
this.setState({
|
||||
width,
|
||||
columnWidth,
|
||||
columnCount,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
rowHeight
|
||||
});
|
||||
};
|
||||
|
||||
cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
posterOptions,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
columnCount
|
||||
} = this.state;
|
||||
|
||||
const {
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showNextAlbum
|
||||
} = posterOptions;
|
||||
|
||||
const artist = items[rowIndex * columnCount + columnIndex];
|
||||
|
||||
if (!artist) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
style={{
|
||||
...style,
|
||||
padding: this._padding
|
||||
}}
|
||||
>
|
||||
<ArtistIndexItemConnector
|
||||
key={artist.id}
|
||||
component={ArtistIndexPoster}
|
||||
sortKey={sortKey}
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
showTitle={showTitle}
|
||||
showMonitored={showMonitored}
|
||||
showQualityProfile={showQualityProfile}
|
||||
showNextAlbum={showNextAlbum}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
longDateFormat={longDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
style={style}
|
||||
artistId={artist.id}
|
||||
qualityProfileId={artist.qualityProfileId}
|
||||
metadataProfileId={artist.metadataProfileId}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMeasure = ({ width }) => {
|
||||
this.calculateGrid(width, this.props.isSmallScreen);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
scroller,
|
||||
items,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
width,
|
||||
columnWidth,
|
||||
columnCount,
|
||||
rowHeight
|
||||
} = this.state;
|
||||
|
||||
const rowCount = Math.ceil(items.length / columnCount);
|
||||
|
||||
return (
|
||||
<Measure
|
||||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<WindowScroller
|
||||
scrollElement={isSmallScreen ? undefined : scroller}
|
||||
>
|
||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||
if (!height) {
|
||||
return <div />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={registerChild}>
|
||||
<Grid
|
||||
ref={this.setGridRef}
|
||||
className={styles.grid}
|
||||
autoHeight={true}
|
||||
height={height}
|
||||
columnCount={columnCount}
|
||||
columnWidth={columnWidth}
|
||||
rowCount={rowCount}
|
||||
rowHeight={rowHeight}
|
||||
width={width}
|
||||
onScroll={onChildScroll}
|
||||
scrollTop={scrollTop}
|
||||
overscanRowCount={2}
|
||||
cellRenderer={this.cellRenderer}
|
||||
scrollToAlignment={'start'}
|
||||
isScrollingOptOut={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
</WindowScroller>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexPosters.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
posterOptions: PropTypes.object.isRequired,
|
||||
jumpToCharacter: PropTypes.string,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexPosters;
|
@ -0,0 +1,294 @@
|
||||
import { throttle } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FixedSizeGrid as Grid, GridChildComponentProps } from 'react-window';
|
||||
import { createSelector } from 'reselect';
|
||||
import Artist from 'Artist/Artist';
|
||||
import ArtistIndexPoster from 'Artist/Index/Posters/ArtistIndexPoster';
|
||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
|
||||
const bodyPadding = parseInt(dimensions.pageContentBodyPadding);
|
||||
const bodyPaddingSmallScreen = parseInt(
|
||||
dimensions.pageContentBodyPaddingSmallScreen
|
||||
);
|
||||
const columnPadding = parseInt(dimensions.artistIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(
|
||||
dimensions.artistIndexColumnPaddingSmallScreen
|
||||
);
|
||||
const progressBarHeight = parseInt(dimensions.progressBarSmallHeight);
|
||||
const detailedProgressBarHeight = parseInt(dimensions.progressBarMediumHeight);
|
||||
|
||||
const ADDITIONAL_COLUMN_COUNT = {
|
||||
small: 3,
|
||||
medium: 2,
|
||||
large: 1,
|
||||
};
|
||||
|
||||
interface CellItemData {
|
||||
layout: {
|
||||
columnCount: number;
|
||||
padding: number;
|
||||
posterWidth: number;
|
||||
posterHeight: number;
|
||||
};
|
||||
items: Artist[];
|
||||
sortKey: string;
|
||||
}
|
||||
|
||||
interface ArtistIndexPostersProps {
|
||||
items: Artist[];
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
const artistIndexSelector = createSelector(
|
||||
(state) => state.artistIndex.posterOptions,
|
||||
(posterOptions) => {
|
||||
return {
|
||||
posterOptions,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const Cell: React.FC<GridChildComponentProps<CellItemData>> = ({
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
style,
|
||||
data,
|
||||
}) => {
|
||||
const { layout, items, sortKey } = data;
|
||||
|
||||
const { columnCount, padding, posterWidth, posterHeight } = layout;
|
||||
|
||||
const index = rowIndex * columnCount + columnIndex;
|
||||
|
||||
if (index >= items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const artist = items[index];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
padding,
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<ArtistIndexPoster
|
||||
artistId={artist.id}
|
||||
sortKey={sortKey}
|
||||
posterWidth={posterWidth}
|
||||
posterHeight={posterHeight}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getWindowScrollTopPosition() {
|
||||
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||
}
|
||||
|
||||
export default function ArtistIndexPosters(props: ArtistIndexPostersProps) {
|
||||
const { scrollerRef, items, sortKey, jumpToCharacter, isSmallScreen } = props;
|
||||
|
||||
const { posterOptions } = useSelector(artistIndexSelector);
|
||||
const ref: React.MutableRefObject<Grid> = useRef();
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
const columnWidth = useMemo(() => {
|
||||
const { width } = size;
|
||||
const maximumColumnWidth = isSmallScreen ? 172 : 182;
|
||||
const columns = Math.floor(width / maximumColumnWidth);
|
||||
const remainder = width % maximumColumnWidth;
|
||||
return remainder === 0
|
||||
? maximumColumnWidth
|
||||
: Math.floor(
|
||||
width / (columns + ADDITIONAL_COLUMN_COUNT[posterOptions.size])
|
||||
);
|
||||
}, [isSmallScreen, posterOptions, size]);
|
||||
|
||||
const columnCount = useMemo(
|
||||
() => Math.max(Math.floor(size.width / columnWidth), 1),
|
||||
[size, columnWidth]
|
||||
);
|
||||
const padding = props.isSmallScreen
|
||||
? columnPaddingSmallScreen
|
||||
: columnPadding;
|
||||
const posterWidth = columnWidth - padding * 2;
|
||||
const posterHeight = Math.ceil(posterWidth);
|
||||
|
||||
const rowHeight = useMemo(() => {
|
||||
const {
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showNextAlbum,
|
||||
} = posterOptions;
|
||||
|
||||
const nextAiringHeight = 19;
|
||||
|
||||
const heights = [
|
||||
posterHeight,
|
||||
detailedProgressBar ? detailedProgressBarHeight : progressBarHeight,
|
||||
nextAiringHeight,
|
||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding,
|
||||
];
|
||||
|
||||
if (showTitle) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showMonitored) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showQualityProfile) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showNextAlbum) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
switch (sortKey) {
|
||||
case 'artistType':
|
||||
case 'metadataProfileId':
|
||||
case 'lastAlbum':
|
||||
case 'added':
|
||||
case 'albumCount':
|
||||
case 'path':
|
||||
case 'sizeOnDisk':
|
||||
case 'tags':
|
||||
heights.push(19);
|
||||
break;
|
||||
case 'qualityProfileId':
|
||||
if (!showQualityProfile) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
case 'nextAlbum':
|
||||
if (!showNextAlbum) {
|
||||
heights.push(19);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// No need to add a height of 0
|
||||
}
|
||||
|
||||
return heights.reduce((acc, height) => acc + height, 0);
|
||||
}, [isSmallScreen, posterOptions, sortKey, posterHeight]);
|
||||
|
||||
useEffect(() => {
|
||||
const current = scrollerRef.current;
|
||||
|
||||
if (isSmallScreen) {
|
||||
const padding = bodyPaddingSmallScreen - 5;
|
||||
|
||||
setSize({
|
||||
width: window.innerWidth - padding * 2,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (current) {
|
||||
const width = current.clientWidth;
|
||||
const padding = bodyPadding - 5;
|
||||
|
||||
setSize({
|
||||
width: width - padding * 2,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
}, [isSmallScreen, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
const scrollTop =
|
||||
(isSmallScreen
|
||||
? getWindowScrollTopPosition()
|
||||
: currentScrollerRef.scrollTop) - offsetTop;
|
||||
|
||||
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
}, 10);
|
||||
|
||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
handleScroll.cancel();
|
||||
|
||||
if (currentScrollListener) {
|
||||
currentScrollListener.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
}, [isSmallScreen, ref, scrollerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (jumpToCharacter) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (index != null) {
|
||||
const rowIndex = Math.floor(index / columnCount);
|
||||
|
||||
const scrollTop = rowIndex * rowHeight + padding;
|
||||
|
||||
ref.current.scrollTo({ scrollLeft: 0, scrollTop });
|
||||
scrollerRef.current.scrollTo(0, scrollTop);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
jumpToCharacter,
|
||||
rowHeight,
|
||||
columnCount,
|
||||
padding,
|
||||
items,
|
||||
scrollerRef,
|
||||
ref,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div ref={measureRef}>
|
||||
<Grid<CellItemData>
|
||||
ref={ref}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'none',
|
||||
}}
|
||||
width={size.width}
|
||||
height={size.height}
|
||||
columnCount={columnCount}
|
||||
columnWidth={columnWidth}
|
||||
rowCount={Math.ceil(items.length / columnCount)}
|
||||
rowHeight={rowHeight}
|
||||
itemData={{
|
||||
layout: {
|
||||
columnCount,
|
||||
padding,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
},
|
||||
items,
|
||||
sortKey,
|
||||
}}
|
||||
>
|
||||
{Cell}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import ArtistIndexPosters from './ArtistIndexPosters';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.artistIndex.posterOptions,
|
||||
createUISettingsSelector(),
|
||||
createDimensionsSelector(),
|
||||
(posterOptions, uiSettings, dimensions) => {
|
||||
return {
|
||||
posterOptions,
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(ArtistIndexPosters);
|
@ -1,25 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ArtistIndexPosterOptionsModalContentConnector from './ArtistIndexPosterOptionsModalContentConnector';
|
||||
|
||||
function ArtistIndexPosterOptionsModal({ isOpen, onModalClose, ...otherProps }) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ArtistIndexPosterOptionsModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ArtistIndexPosterOptionsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexPosterOptionsModal;
|
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ArtistIndexPosterOptionsModalContent from './ArtistIndexPosterOptionsModalContent';
|
||||
|
||||
interface ArtistIndexPosterOptionsModalProps {
|
||||
isOpen: boolean;
|
||||
onModalClose(...args: unknown[]): unknown;
|
||||
}
|
||||
|
||||
function ArtistIndexPosterOptionsModal({
|
||||
isOpen,
|
||||
onModalClose,
|
||||
}: ArtistIndexPosterOptionsModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||
<ArtistIndexPosterOptionsModalContent onModalClose={onModalClose} />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexPosterOptionsModal;
|
@ -1,248 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const posterSizeOptions = [
|
||||
{ key: 'small', value: 'Small' },
|
||||
{ key: 'medium', value: 'Medium' },
|
||||
{ key: 'large', value: 'Large' }
|
||||
];
|
||||
|
||||
class ArtistIndexPosterOptionsModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
detailedProgressBar: props.detailedProgressBar,
|
||||
size: props.size,
|
||||
showTitle: props.showTitle,
|
||||
showMonitored: props.showMonitored,
|
||||
showQualityProfile: props.showQualityProfile,
|
||||
showNextAlbum: props.showNextAlbum,
|
||||
showSearchAction: props.showSearchAction
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
detailedProgressBar,
|
||||
size,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showNextAlbum,
|
||||
showSearchAction
|
||||
} = this.props;
|
||||
|
||||
const state = {};
|
||||
|
||||
if (detailedProgressBar !== prevProps.detailedProgressBar) {
|
||||
state.detailedProgressBar = detailedProgressBar;
|
||||
}
|
||||
|
||||
if (size !== prevProps.size) {
|
||||
state.size = size;
|
||||
}
|
||||
|
||||
if (showTitle !== prevProps.showTitle) {
|
||||
state.showTitle = showTitle;
|
||||
}
|
||||
|
||||
if (showMonitored !== prevProps.showMonitored) {
|
||||
state.showMonitored = showMonitored;
|
||||
}
|
||||
|
||||
if (showQualityProfile !== prevProps.showQualityProfile) {
|
||||
state.showQualityProfile = showQualityProfile;
|
||||
}
|
||||
|
||||
if (showNextAlbum !== prevProps.showNextAlbum) {
|
||||
state.showNextAlbum = showNextAlbum;
|
||||
}
|
||||
|
||||
if (showSearchAction !== prevProps.showSearchAction) {
|
||||
state.showSearchAction = showSearchAction;
|
||||
}
|
||||
|
||||
if (!_.isEmpty(state)) {
|
||||
this.setState(state);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChangePosterOption = ({ name, value }) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
}, () => {
|
||||
this.props.onChangePosterOption({ [name]: value });
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
detailedProgressBar,
|
||||
size,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showNextAlbum,
|
||||
showSearchAction
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Poster Options
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('PosterSize')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="size"
|
||||
value={size}
|
||||
values={posterSizeOptions}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('DetailedProgressBar')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="detailedProgressBar"
|
||||
value={detailedProgressBar}
|
||||
helpText={translate('DetailedProgressBarHelpText')}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowName')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showTitle"
|
||||
value={showTitle}
|
||||
helpText={translate('ShowTitleHelpText')}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowMonitored')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showMonitored"
|
||||
value={showMonitored}
|
||||
helpText={translate('ShowMonitoredHelpText')}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowQualityProfile')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showQualityProfile"
|
||||
value={showQualityProfile}
|
||||
helpText={translate('ShowQualityProfileHelpText')}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowNextAlbum')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showNextAlbum"
|
||||
value={showNextAlbum}
|
||||
helpText={translate('ShowNextAlbumHelpText')}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowSearch')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showSearchAction"
|
||||
value={showSearchAction}
|
||||
helpText={translate('ShowSearchActionHelpText')}
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexPosterOptionsModalContent.propTypes = {
|
||||
size: PropTypes.string.isRequired,
|
||||
showTitle: PropTypes.bool.isRequired,
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
showNextAlbum: PropTypes.bool.isRequired,
|
||||
detailedProgressBar: PropTypes.bool.isRequired,
|
||||
showSearchAction: PropTypes.bool.isRequired,
|
||||
onChangePosterOption: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexPosterOptionsModalContent;
|
@ -0,0 +1,167 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import selectPosterOptions from 'Artist/Index/Posters/selectPosterOptions';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import { setArtistPosterOption } from 'Store/Actions/artistIndexActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const posterSizeOptions = [
|
||||
{
|
||||
key: 'small',
|
||||
get value() {
|
||||
return translate('Small');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'medium',
|
||||
get value() {
|
||||
return translate('Medium');
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'large',
|
||||
get value() {
|
||||
return translate('Large');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
interface ArtistIndexPosterOptionsModalContentProps {
|
||||
onModalClose(...args: unknown[]): unknown;
|
||||
}
|
||||
|
||||
function ArtistIndexPosterOptionsModalContent(
|
||||
props: ArtistIndexPosterOptionsModalContentProps
|
||||
) {
|
||||
const { onModalClose } = props;
|
||||
|
||||
const posterOptions = useSelector(selectPosterOptions);
|
||||
|
||||
const {
|
||||
detailedProgressBar,
|
||||
size,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showNextAlbum,
|
||||
showSearchAction,
|
||||
} = posterOptions;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onPosterOptionChange = useCallback(
|
||||
({ name, value }) => {
|
||||
dispatch(setArtistPosterOption({ [name]: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>{translate('PosterOptions')}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PosterSize')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="size"
|
||||
value={size}
|
||||
values={posterSizeOptions}
|
||||
onChange={onPosterOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('DetailedProgressBar')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="detailedProgressBar"
|
||||
value={detailedProgressBar}
|
||||
helpText={translate('DetailedProgressBarHelpText')}
|
||||
onChange={onPosterOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowName')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showTitle"
|
||||
value={showTitle}
|
||||
helpText={translate('ShowTitleHelpText')}
|
||||
onChange={onPosterOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowMonitored')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showMonitored"
|
||||
value={showMonitored}
|
||||
helpText={translate('ShowMonitoredHelpText')}
|
||||
onChange={onPosterOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowQualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showQualityProfile"
|
||||
value={showQualityProfile}
|
||||
helpText={translate('ShowQualityProfileHelpText')}
|
||||
onChange={onPosterOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowNextAlbum')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showNextAlbum"
|
||||
value={showNextAlbum}
|
||||
helpText={translate('ShowNextAlbumHelpText')}
|
||||
onChange={onPosterOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showSearchAction"
|
||||
value={showSearchAction}
|
||||
helpText={translate('ShowSearchActionHelpText')}
|
||||
onChange={onPosterOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexPosterOptionsModalContent;
|
@ -1,23 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setArtistPosterOption } from 'Store/Actions/artistIndexActions';
|
||||
import ArtistIndexPosterOptionsModalContent from './ArtistIndexPosterOptionsModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.artistIndex,
|
||||
(artistIndex) => {
|
||||
return artistIndex.posterOptions;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onChangePosterOption(payload) {
|
||||
dispatch(setArtistPosterOption(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexPosterOptionsModalContent);
|
@ -0,0 +1,9 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
const selectPosterOptions = createSelector(
|
||||
(state: AppState) => state.artistIndex.posterOptions,
|
||||
(posterOptions) => posterOptions
|
||||
);
|
||||
|
||||
export default selectPosterOptions;
|
@ -1,103 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class ArtistIndexActionsCell extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditArtistPress = () => {
|
||||
this.setState({ isEditArtistModalOpen: true });
|
||||
};
|
||||
|
||||
onEditArtistModalClose = () => {
|
||||
this.setState({ isEditArtistModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteArtistPress = () => {
|
||||
this.setState({
|
||||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteArtistModalClose = () => {
|
||||
this.setState({ isDeleteArtistModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
isRefreshingArtist,
|
||||
onRefreshArtistPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isEditArtistModalOpen,
|
||||
isDeleteArtistModalOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
{...otherProps}
|
||||
>
|
||||
<SpinnerIconButton
|
||||
name={icons.REFRESH}
|
||||
title={translate('RefreshArtist')}
|
||||
isSpinning={isRefreshingArtist}
|
||||
onPress={onRefreshArtistPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={icons.EDIT}
|
||||
title={translate('EditArtist')}
|
||||
onPress={this.onEditArtistPress}
|
||||
/>
|
||||
|
||||
<EditArtistModalConnector
|
||||
isOpen={isEditArtistModalOpen}
|
||||
artistId={id}
|
||||
onModalClose={this.onEditArtistModalClose}
|
||||
onDeleteArtistPress={this.onDeleteArtistPress}
|
||||
/>
|
||||
|
||||
<DeleteArtistModal
|
||||
isOpen={isDeleteArtistModalOpen}
|
||||
artistId={id}
|
||||
onModalClose={this.onDeleteArtistModalClose}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexActionsCell.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
isRefreshingArtist: PropTypes.bool.isRequired,
|
||||
onRefreshArtistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexActionsCell;
|
@ -1,86 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import ArtistIndexTableOptionsConnector from './ArtistIndexTableOptionsConnector';
|
||||
import hasGrowableColumns from './hasGrowableColumns';
|
||||
import styles from './ArtistIndexHeader.css';
|
||||
|
||||
function ArtistIndexHeader(props) {
|
||||
const {
|
||||
showBanners,
|
||||
columns,
|
||||
onTableOptionChange,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<VirtualTableHeader>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
isSortable,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
name={name}
|
||||
isSortable={false}
|
||||
{...otherProps}
|
||||
>
|
||||
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
optionsComponent={ArtistIndexTableOptionsConnector}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<IconButton
|
||||
name={icons.ADVANCED_SETTINGS}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
key={name}
|
||||
className={classNames(
|
||||
styles[name],
|
||||
name === 'sortName' && showBanners && styles.banner,
|
||||
name === 'sortName' && showBanners && !hasGrowableColumns(columns) && styles.bannerGrow
|
||||
)}
|
||||
name={name}
|
||||
isSortable={isSortable}
|
||||
{...otherProps}
|
||||
>
|
||||
{typeof label === 'function' ? label() : label}
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
})
|
||||
}
|
||||
</VirtualTableHeader>
|
||||
);
|
||||
}
|
||||
|
||||
ArtistIndexHeader.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
showBanners: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexHeader;
|
@ -1,13 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { setArtistTableOption } from 'Store/Actions/artistIndexActions';
|
||||
import ArtistIndexHeader from './ArtistIndexHeader';
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onTableOptionChange(payload) {
|
||||
dispatch(setArtistTableOption(payload));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(undefined, createMapDispatchToProps)(ArtistIndexHeader);
|
@ -1,487 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
||||
import ArtistBanner from 'Artist/ArtistBanner';
|
||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import getProgressBarKind from 'Utilities/Artist/getProgressBarKind';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ArtistStatusCell from './ArtistStatusCell';
|
||||
import hasGrowableColumns from './hasGrowableColumns';
|
||||
import styles from './ArtistIndexRow.css';
|
||||
|
||||
class ArtistIndexRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
hasBannerError: false,
|
||||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
onEditArtistPress = () => {
|
||||
this.setState({ isEditArtistModalOpen: true });
|
||||
};
|
||||
|
||||
onEditArtistModalClose = () => {
|
||||
this.setState({ isEditArtistModalOpen: false });
|
||||
};
|
||||
|
||||
onDeleteArtistPress = () => {
|
||||
this.setState({
|
||||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: true
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteArtistModalClose = () => {
|
||||
this.setState({ isDeleteArtistModalOpen: false });
|
||||
};
|
||||
|
||||
onUseSceneNumberingChange = () => {
|
||||
// Mock handler to satisfy `onChange` being required for `CheckInput`.
|
||||
//
|
||||
};
|
||||
|
||||
onBannerLoad = () => {
|
||||
if (this.state.hasBannerError) {
|
||||
this.setState({ hasBannerError: false });
|
||||
}
|
||||
};
|
||||
|
||||
onBannerLoadError = () => {
|
||||
if (!this.state.hasBannerError) {
|
||||
this.setState({ hasBannerError: true });
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
monitored,
|
||||
status,
|
||||
artistName,
|
||||
foreignArtistId,
|
||||
artistType,
|
||||
qualityProfile,
|
||||
metadataProfile,
|
||||
nextAlbum,
|
||||
lastAlbum,
|
||||
added,
|
||||
statistics,
|
||||
genres,
|
||||
ratings,
|
||||
path,
|
||||
tags,
|
||||
images,
|
||||
isSaving,
|
||||
showBanners,
|
||||
showSearchAction,
|
||||
columns,
|
||||
isRefreshingArtist,
|
||||
isSearchingArtist,
|
||||
onRefreshArtistPress,
|
||||
onSearchPress,
|
||||
onMonitoredPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
albumCount,
|
||||
trackCount,
|
||||
trackFileCount,
|
||||
totalTrackCount,
|
||||
sizeOnDisk
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
hasBannerError,
|
||||
isEditArtistModalOpen,
|
||||
isDeleteArtistModalOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<ArtistStatusCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
artistType={artistType}
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
isSaving={isSaving}
|
||||
onMonitoredPress={onMonitoredPress}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sortName') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={classNames(
|
||||
styles[name],
|
||||
showBanners && styles.banner,
|
||||
showBanners && !hasGrowableColumns(columns) && styles.bannerGrow
|
||||
)}
|
||||
>
|
||||
{
|
||||
showBanners ?
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`/artist/${foreignArtistId}`}
|
||||
>
|
||||
<ArtistBanner
|
||||
className={styles.bannerImage}
|
||||
images={images}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
onError={this.onBannerLoadError}
|
||||
onLoad={this.onBannerLoad}
|
||||
/>
|
||||
|
||||
{
|
||||
hasBannerError &&
|
||||
<div className={styles.overlayTitle}>
|
||||
{artistName}
|
||||
</div>
|
||||
}
|
||||
</Link> :
|
||||
|
||||
<ArtistNameLink
|
||||
foreignArtistId={foreignArtistId}
|
||||
artistName={artistName}
|
||||
/>
|
||||
}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'artistType') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{artistType}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{qualityProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'metadataProfileId') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{metadataProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'nextAlbum') {
|
||||
if (nextAlbum) {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<AlbumTitleLink
|
||||
title={nextAlbum.title}
|
||||
disambiguation={nextAlbum.disambiguation}
|
||||
foreignAlbumId={nextAlbum.foreignAlbumId}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
None
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'lastAlbum') {
|
||||
if (lastAlbum) {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<AlbumTitleLink
|
||||
title={lastAlbum.title}
|
||||
disambiguation={lastAlbum.disambiguation}
|
||||
foreignAlbumId={lastAlbum.foreignAlbumId}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
None
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={added}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'albumCount') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{albumCount}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'trackProgress') {
|
||||
const progress = trackCount ? trackFileCount / trackCount * 100 : 100;
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
kind={getProgressBarKind(status, monitored, progress)}
|
||||
showText={true}
|
||||
text={`${trackFileCount} / ${trackCount}`}
|
||||
title={translate('TrackFileCountTrackCountTotalTotalTrackCountInterp', [trackFileCount, trackCount, totalTrackCount])}
|
||||
width={125}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'trackCount') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{totalTrackCount}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'path') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{path}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sizeOnDisk') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
{formatBytes(sizeOnDisk)}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'genres') {
|
||||
const joinedGenres = genres.join(', ');
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<span title={joinedGenres}>
|
||||
{joinedGenres}
|
||||
</span>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'ratings') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<HeartRating
|
||||
rating={ratings.value}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'tags') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<TagListConnector
|
||||
tags={tags}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
>
|
||||
<SpinnerIconButton
|
||||
name={icons.REFRESH}
|
||||
title={translate('RefreshArtist')}
|
||||
isSpinning={isRefreshingArtist}
|
||||
onPress={onRefreshArtistPress}
|
||||
/>
|
||||
|
||||
{
|
||||
showSearchAction &&
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
name={icons.SEARCH}
|
||||
title={translate('SearchForMonitoredAlbums')}
|
||||
isSpinning={isSearchingArtist}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
}
|
||||
|
||||
<IconButton
|
||||
name={icons.EDIT}
|
||||
title={translate('EditArtist')}
|
||||
onPress={this.onEditArtistPress}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<EditArtistModalConnector
|
||||
isOpen={isEditArtistModalOpen}
|
||||
artistId={id}
|
||||
onModalClose={this.onEditArtistModalClose}
|
||||
onDeleteArtistPress={this.onDeleteArtistPress}
|
||||
/>
|
||||
|
||||
<DeleteArtistModal
|
||||
isOpen={isDeleteArtistModalOpen}
|
||||
artistId={id}
|
||||
onModalClose={this.onDeleteArtistModalClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
artistName: PropTypes.string.isRequired,
|
||||
foreignArtistId: PropTypes.string.isRequired,
|
||||
artistType: PropTypes.string,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
metadataProfile: PropTypes.object.isRequired,
|
||||
nextAlbum: PropTypes.object,
|
||||
lastAlbum: PropTypes.object,
|
||||
added: PropTypes.string,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
latestAlbum: PropTypes.object,
|
||||
path: PropTypes.string.isRequired,
|
||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
showBanners: PropTypes.bool.isRequired,
|
||||
showSearchAction: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isRefreshingArtist: PropTypes.bool.isRequired,
|
||||
isSearchingArtist: PropTypes.bool.isRequired,
|
||||
onRefreshArtistPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
onMonitoredPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ArtistIndexRow.defaultProps = {
|
||||
statistics: {
|
||||
albumCount: 0,
|
||||
trackCount: 0,
|
||||
trackFileCount: 0,
|
||||
totalTrackCount: 0
|
||||
},
|
||||
genres: [],
|
||||
tags: []
|
||||
};
|
||||
|
||||
export default ArtistIndexRow;
|
@ -0,0 +1,392 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
||||
import { Statistics } from 'Artist/Artist';
|
||||
import ArtistBanner from 'Artist/ArtistBanner';
|
||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||
import createArtistIndexItemSelector from 'Artist/Index/createArtistIndexItemSelector';
|
||||
import ArtistStatusCell from 'Artist/Index/Table/ArtistStatusCell';
|
||||
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import Column from 'Components/Table/Column';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import getProgressBarKind from 'Utilities/Artist/getProgressBarKind';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import hasGrowableColumns from './hasGrowableColumns';
|
||||
import selectTableOptions from './selectTableOptions';
|
||||
import styles from './ArtistIndexRow.css';
|
||||
|
||||
interface ArtistIndexRowProps {
|
||||
artistId: number;
|
||||
sortKey: string;
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
function ArtistIndexRow(props: ArtistIndexRowProps) {
|
||||
const { artistId, columns } = props;
|
||||
|
||||
const {
|
||||
artist,
|
||||
qualityProfile,
|
||||
metadataProfile,
|
||||
isRefreshingArtist,
|
||||
isSearchingArtist,
|
||||
} = useSelector(createArtistIndexItemSelector(props.artistId));
|
||||
|
||||
const { showBanners, showSearchAction } = useSelector(selectTableOptions);
|
||||
|
||||
const {
|
||||
artistName,
|
||||
foreignArtistId,
|
||||
monitored,
|
||||
status,
|
||||
path,
|
||||
nextAlbum,
|
||||
lastAlbum,
|
||||
added,
|
||||
statistics = {} as Statistics,
|
||||
images,
|
||||
artistType,
|
||||
genres = [],
|
||||
ratings,
|
||||
tags = [],
|
||||
isSaving = false,
|
||||
} = artist;
|
||||
|
||||
const {
|
||||
albumCount = 0,
|
||||
trackCount = 0,
|
||||
trackFileCount = 0,
|
||||
totalTrackCount = 0,
|
||||
sizeOnDisk = 0,
|
||||
} = statistics;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [hasBannerError, setHasBannerError] = useState(false);
|
||||
const [isEditArtistModalOpen, setIsEditArtistModalOpen] = useState(false);
|
||||
const [isDeleteArtistModalOpen, setIsDeleteArtistModalOpen] = useState(false);
|
||||
|
||||
const onRefreshPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: REFRESH_ARTIST,
|
||||
artistId,
|
||||
})
|
||||
);
|
||||
}, [artistId, dispatch]);
|
||||
|
||||
const onSearchPress = useCallback(() => {
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: ARTIST_SEARCH,
|
||||
artistId,
|
||||
})
|
||||
);
|
||||
}, [artistId, dispatch]);
|
||||
|
||||
const onBannerLoadError = useCallback(() => {
|
||||
setHasBannerError(true);
|
||||
}, [setHasBannerError]);
|
||||
|
||||
const onBannerLoad = useCallback(() => {
|
||||
setHasBannerError(false);
|
||||
}, [setHasBannerError]);
|
||||
|
||||
const onEditArtistPress = useCallback(() => {
|
||||
setIsEditArtistModalOpen(true);
|
||||
}, [setIsEditArtistModalOpen]);
|
||||
|
||||
const onEditArtistModalClose = useCallback(() => {
|
||||
setIsEditArtistModalOpen(false);
|
||||
}, [setIsEditArtistModalOpen]);
|
||||
|
||||
const onDeleteArtistPress = useCallback(() => {
|
||||
setIsEditArtistModalOpen(false);
|
||||
setIsDeleteArtistModalOpen(true);
|
||||
}, [setIsDeleteArtistModalOpen]);
|
||||
|
||||
const onDeleteArtistModalClose = useCallback(() => {
|
||||
setIsDeleteArtistModalOpen(false);
|
||||
}, [setIsDeleteArtistModalOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{columns.map((column) => {
|
||||
const { name, isVisible } = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<ArtistStatusCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
artistId={artistId}
|
||||
artistType={artistType}
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
isSaving={isSaving}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sortName') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={classNames(
|
||||
styles[name],
|
||||
showBanners && styles.banner,
|
||||
showBanners && !hasGrowableColumns(columns) && styles.bannerGrow
|
||||
)}
|
||||
>
|
||||
{showBanners ? (
|
||||
<Link className={styles.link} to={`/artist/${foreignArtistId}`}>
|
||||
<ArtistBanner
|
||||
className={styles.bannerImage}
|
||||
images={images}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
onError={onBannerLoadError}
|
||||
onLoad={onBannerLoad}
|
||||
/>
|
||||
|
||||
{hasBannerError && (
|
||||
<div className={styles.overlayTitle}>{artistName}</div>
|
||||
)}
|
||||
</Link>
|
||||
) : (
|
||||
<ArtistNameLink
|
||||
foreignArtistId={foreignArtistId}
|
||||
artistName={artistName}
|
||||
/>
|
||||
)}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'artistType') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{artistType}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{qualityProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{qualityProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'metadataProfileId') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{metadataProfile.name}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'nextAlbum') {
|
||||
if (nextAlbum) {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<AlbumTitleLink
|
||||
title={nextAlbum.title}
|
||||
disambiguation={nextAlbum.disambiguation}
|
||||
foreignAlbumId={nextAlbum.foreignAlbumId}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
None
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'lastAlbum') {
|
||||
if (lastAlbum) {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<AlbumTitleLink
|
||||
title={lastAlbum.title}
|
||||
disambiguation={lastAlbum.disambiguation}
|
||||
foreignAlbumId={lastAlbum.foreignAlbumId}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
None
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
date={added}
|
||||
component={VirtualTableRowCell}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'albumCount') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{albumCount}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'trackProgress') {
|
||||
const progress = trackCount
|
||||
? (trackFileCount / trackCount) * 100
|
||||
: 100;
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
kind={getProgressBarKind(status, monitored, progress)}
|
||||
showText={true}
|
||||
text={`${trackFileCount} / ${trackCount}`}
|
||||
title={translate('ArtistProgressBarText', {
|
||||
trackFileCount,
|
||||
trackCount,
|
||||
totalTrackCount,
|
||||
})}
|
||||
width={125}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'trackCount') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{totalTrackCount}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'path') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{path}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sizeOnDisk') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
{formatBytes(sizeOnDisk)}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'genres') {
|
||||
const joinedGenres = genres.join(', ');
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<span title={joinedGenres}>{joinedGenres}</span>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'ratings') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<HeartRating rating={ratings.value} />
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'tags') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<TagListConnector tags={tags} />
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<SpinnerIconButton
|
||||
name={icons.REFRESH}
|
||||
title={translate('RefreshArtist')}
|
||||
isSpinning={isRefreshingArtist}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
|
||||
{showSearchAction ? (
|
||||
<SpinnerIconButton
|
||||
name={icons.SEARCH}
|
||||
title={translate('SearchForMonitoredAlbums')}
|
||||
isSpinning={isSearchingArtist}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<IconButton
|
||||
name={icons.EDIT}
|
||||
title={translate('EditArtist')}
|
||||
onPress={onEditArtistPress}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})}
|
||||
|
||||
<EditArtistModalConnector
|
||||
isOpen={isEditArtistModalOpen}
|
||||
artistId={artistId}
|
||||
onModalClose={onEditArtistModalClose}
|
||||
onDeleteArtistPress={onDeleteArtistPress}
|
||||
/>
|
||||
|
||||
<DeleteArtistModal
|
||||
isOpen={isDeleteArtistModalOpen}
|
||||
artistId={artistId}
|
||||
onModalClose={onDeleteArtistModalClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexRow;
|
@ -1,5 +1,3 @@
|
||||
.tableContainer {
|
||||
composes: tableContainer from '~Components/Table/VirtualTable.css';
|
||||
|
||||
flex: 1 0 auto;
|
||||
.tableScroller {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'tableContainer': string;
|
||||
'tableScroller': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
@ -1,134 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ArtistIndexItemConnector from 'Artist/Index/ArtistIndexItemConnector';
|
||||
import VirtualTable from 'Components/Table/VirtualTable';
|
||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
import ArtistIndexHeaderConnector from './ArtistIndexHeaderConnector';
|
||||
import ArtistIndexRow from './ArtistIndexRow';
|
||||
import styles from './ArtistIndexTable.css';
|
||||
|
||||
class ArtistIndexTable extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
scrollIndex: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
jumpToCharacter
|
||||
} = this.props;
|
||||
|
||||
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
|
||||
|
||||
const scrollIndex = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (scrollIndex != null) {
|
||||
this.setState({ scrollIndex });
|
||||
}
|
||||
} else if (jumpToCharacter == null && prevProps.jumpToCharacter != null) {
|
||||
this.setState({ scrollIndex: null });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
rowRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
items,
|
||||
columns,
|
||||
showBanners,
|
||||
isSaving
|
||||
} = this.props;
|
||||
|
||||
const artist = items[rowIndex];
|
||||
|
||||
return (
|
||||
<VirtualTableRow
|
||||
key={key}
|
||||
style={style}
|
||||
>
|
||||
<ArtistIndexItemConnector
|
||||
key={artist.id}
|
||||
component={ArtistIndexRow}
|
||||
style={style}
|
||||
columns={columns}
|
||||
artistId={artist.id}
|
||||
qualityProfileId={artist.qualityProfileId}
|
||||
metadataProfileId={artist.metadataProfileId}
|
||||
showBanners={showBanners}
|
||||
isSaving={isSaving}
|
||||
/>
|
||||
</VirtualTableRow>
|
||||
);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
columns,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
showBanners,
|
||||
isSmallScreen,
|
||||
onSortPress,
|
||||
scroller,
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<VirtualTable
|
||||
className={styles.tableContainer}
|
||||
items={items}
|
||||
scrollIndex={this.state.scrollIndex}
|
||||
scrollTop={scrollTop}
|
||||
isSmallScreen={isSmallScreen}
|
||||
scroller={scroller}
|
||||
rowHeight={showBanners ? 70 : 38}
|
||||
overscanRowCount={2}
|
||||
rowRenderer={this.rowRenderer}
|
||||
header={
|
||||
<ArtistIndexHeaderConnector
|
||||
showBanners={showBanners}
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
/>
|
||||
}
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexTable.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
showBanners: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
jumpToCharacter: PropTypes.string,
|
||||
scrollTop: PropTypes.number,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexTable;
|
@ -0,0 +1,205 @@
|
||||
import { throttle } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { FixedSizeList as List, ListChildComponentProps } from 'react-window';
|
||||
import { createSelector } from 'reselect';
|
||||
import Artist from 'Artist/Artist';
|
||||
import ArtistIndexRow from 'Artist/Index/Table/ArtistIndexRow';
|
||||
import ArtistIndexTableHeader from 'Artist/Index/Table/ArtistIndexTableHeader';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import Column from 'Components/Table/Column';
|
||||
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
|
||||
import selectTableOptions from './selectTableOptions';
|
||||
import styles from './ArtistIndexTable.css';
|
||||
|
||||
const bodyPadding = parseInt(dimensions.pageContentBodyPadding);
|
||||
const bodyPaddingSmallScreen = parseInt(
|
||||
dimensions.pageContentBodyPaddingSmallScreen
|
||||
);
|
||||
|
||||
interface RowItemData {
|
||||
items: Artist[];
|
||||
sortKey: string;
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
interface ArtistIndexTableProps {
|
||||
items: Artist[];
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
jumpToCharacter?: string;
|
||||
scrollTop?: number;
|
||||
scrollerRef: React.MutableRefObject<HTMLElement>;
|
||||
isSmallScreen: boolean;
|
||||
}
|
||||
|
||||
const columnsSelector = createSelector(
|
||||
(state) => state.artistIndex.columns,
|
||||
(columns) => columns
|
||||
);
|
||||
|
||||
const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
|
||||
index,
|
||||
style,
|
||||
data,
|
||||
}) => {
|
||||
const { items, sortKey, columns } = data;
|
||||
|
||||
if (index >= items.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const artist = items[index];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
<ArtistIndexRow
|
||||
artistId={artist.id}
|
||||
sortKey={sortKey}
|
||||
columns={columns}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getWindowScrollTopPosition() {
|
||||
return document.documentElement.scrollTop || document.body.scrollTop || 0;
|
||||
}
|
||||
|
||||
function ArtistIndexTable(props: ArtistIndexTableProps) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
jumpToCharacter,
|
||||
isSmallScreen,
|
||||
scrollerRef,
|
||||
} = props;
|
||||
|
||||
const columns = useSelector(columnsSelector);
|
||||
const { showBanners } = useSelector(selectTableOptions);
|
||||
const listRef: React.MutableRefObject<List> = useRef();
|
||||
const [measureRef, bounds] = useMeasure();
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
const rowHeight = useMemo(() => {
|
||||
return showBanners ? 70 : 38;
|
||||
}, [showBanners]);
|
||||
|
||||
useEffect(() => {
|
||||
const current = scrollerRef.current as HTMLElement;
|
||||
|
||||
if (isSmallScreen) {
|
||||
setSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (current) {
|
||||
const width = current.clientWidth;
|
||||
const padding =
|
||||
(isSmallScreen ? bodyPaddingSmallScreen : bodyPadding) - 5;
|
||||
|
||||
setSize({
|
||||
width: width - padding * 2,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
}, [isSmallScreen, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollListener = isSmallScreen ? window : scrollerRef.current;
|
||||
const currentScrollerRef = scrollerRef.current;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const { offsetTop = 0 } = currentScrollerRef;
|
||||
const scrollTop =
|
||||
(isSmallScreen
|
||||
? getWindowScrollTopPosition()
|
||||
: currentScrollerRef.scrollTop) - offsetTop;
|
||||
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
}, 10);
|
||||
|
||||
currentScrollListener.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
handleScroll.cancel();
|
||||
|
||||
if (currentScrollListener) {
|
||||
currentScrollListener.removeEventListener('scroll', handleScroll);
|
||||
}
|
||||
};
|
||||
}, [isSmallScreen, listRef, scrollerRef]);
|
||||
|
||||
useEffect(() => {
|
||||
if (jumpToCharacter) {
|
||||
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
|
||||
|
||||
if (index != null) {
|
||||
let scrollTop = index * rowHeight;
|
||||
|
||||
// If the offset is zero go to the top, otherwise offset
|
||||
// by the approximate size of the header + padding (37 + 20).
|
||||
if (scrollTop > 0) {
|
||||
const offset = 57;
|
||||
|
||||
scrollTop += offset;
|
||||
}
|
||||
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
scrollerRef.current.scrollTo(0, scrollTop);
|
||||
}
|
||||
}
|
||||
}, [jumpToCharacter, rowHeight, items, scrollerRef, listRef]);
|
||||
|
||||
return (
|
||||
<div ref={measureRef}>
|
||||
<Scroller
|
||||
className={styles.tableScroller}
|
||||
scrollDirection={ScrollDirection.Horizontal}
|
||||
>
|
||||
<ArtistIndexTableHeader
|
||||
showBanners={showBanners}
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
/>
|
||||
<List<RowItemData>
|
||||
ref={listRef}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'none',
|
||||
}}
|
||||
width={size.width}
|
||||
height={size.height}
|
||||
itemCount={items.length}
|
||||
itemSize={rowHeight}
|
||||
itemData={{
|
||||
items,
|
||||
sortKey,
|
||||
columns,
|
||||
}}
|
||||
>
|
||||
{Row}
|
||||
</List>
|
||||
</Scroller>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexTable;
|
@ -1,29 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setArtistSort } from 'Store/Actions/artistIndexActions';
|
||||
import ArtistIndexTable from './ArtistIndexTable';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.app.dimensions,
|
||||
(state) => state.artistIndex.tableOptions,
|
||||
(state) => state.artistIndex.columns,
|
||||
(dimensions, tableOptions, columns) => {
|
||||
return {
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
showBanners: tableOptions.showBanners,
|
||||
columns
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onSortPress(sortKey) {
|
||||
dispatch(setArtistSort({ sortKey }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexTable);
|
@ -0,0 +1,98 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import ArtistIndexTableOptions from 'Artist/Index/Table/ArtistIndexTableOptions';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Column from 'Components/Table/Column';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import {
|
||||
setArtistSort,
|
||||
setArtistTableOption,
|
||||
} from 'Store/Actions/artistIndexActions';
|
||||
import hasGrowableColumns from './hasGrowableColumns';
|
||||
import styles from './ArtistIndexTableHeader.css';
|
||||
|
||||
interface ArtistIndexTableHeaderProps {
|
||||
showBanners: boolean;
|
||||
columns: Column[];
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
}
|
||||
|
||||
function ArtistIndexTableHeader(props: ArtistIndexTableHeaderProps) {
|
||||
const { showBanners, columns, sortKey, sortDirection } = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const onSortPress = useCallback(
|
||||
(value) => {
|
||||
dispatch(setArtistSort({ sortKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onTableOptionChange = useCallback(
|
||||
(payload) => {
|
||||
dispatch(setArtistTableOption(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<VirtualTableHeader>
|
||||
{columns.map((column) => {
|
||||
const { name, label, isSortable, isVisible } = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
name={name}
|
||||
isSortable={false}
|
||||
>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
optionsComponent={ArtistIndexTableOptions}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<IconButton name={icons.ADVANCED_SETTINGS} />
|
||||
</TableOptionsModalWrapper>
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
key={name}
|
||||
className={classNames(
|
||||
styles[name],
|
||||
name === 'sortName' && showBanners && styles.banner,
|
||||
name === 'sortName' &&
|
||||
showBanners &&
|
||||
!hasGrowableColumns(columns) &&
|
||||
styles.bannerGrow
|
||||
)}
|
||||
name={name}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
isSortable={isSortable}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
{typeof label === 'function' ? label() : label}
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
})}
|
||||
</VirtualTableHeader>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexTableHeader;
|
@ -1,105 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class ArtistIndexTableOptions extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
showBanners: props.showBanners,
|
||||
showSearchAction: props.showSearchAction
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
showBanners,
|
||||
showSearchAction
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
showBanners !== prevProps.showBanners ||
|
||||
showSearchAction !== prevProps.showSearchAction
|
||||
) {
|
||||
this.setState({
|
||||
showBanners,
|
||||
showSearchAction
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTableOptionChange = ({ name, value }) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
}, () => {
|
||||
this.props.onTableOptionChange({
|
||||
tableOptions: {
|
||||
...this.state,
|
||||
[name]: value
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
showBanners,
|
||||
showSearchAction
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowBanners')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showBanners"
|
||||
value={showBanners}
|
||||
helpText={translate('ShowBannersHelpText')}
|
||||
onChange={this.onTableOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('ShowSearch')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showSearchAction"
|
||||
value={showSearchAction}
|
||||
helpText={translate('ShowSearchActionHelpText')}
|
||||
onChange={this.onTableOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexTableOptions.propTypes = {
|
||||
showBanners: PropTypes.bool.isRequired,
|
||||
showSearchAction: PropTypes.bool.isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexTableOptions;
|
@ -0,0 +1,62 @@
|
||||
import React, { Fragment, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import selectTableOptions from './selectTableOptions';
|
||||
|
||||
interface ArtistIndexTableOptionsProps {
|
||||
onTableOptionChange(...args: unknown[]): unknown;
|
||||
}
|
||||
|
||||
function ArtistIndexTableOptions(props: ArtistIndexTableOptionsProps) {
|
||||
const { onTableOptionChange } = props;
|
||||
|
||||
const tableOptions = useSelector(selectTableOptions);
|
||||
|
||||
const { showBanners, showSearchAction } = tableOptions;
|
||||
|
||||
const onTableOptionChangeWrapper = useCallback(
|
||||
({ name, value }) => {
|
||||
onTableOptionChange({
|
||||
tableOptions: {
|
||||
...tableOptions,
|
||||
[name]: value,
|
||||
},
|
||||
});
|
||||
},
|
||||
[tableOptions, onTableOptionChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowBanners')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showBanners"
|
||||
value={showBanners}
|
||||
helpText={translate('ShowBannersHelpText')}
|
||||
onChange={onTableOptionChangeWrapper}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowSearch')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showSearchAction"
|
||||
value={showSearchAction}
|
||||
helpText={translate('ShowSearchActionHelpText')}
|
||||
onChange={onTableOptionChangeWrapper}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArtistIndexTableOptions;
|
@ -1,14 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import ArtistIndexTableOptions from './ArtistIndexTableOptions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.artistIndex.tableOptions,
|
||||
(tableOptions) => {
|
||||
return tableOptions;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(ArtistIndexTableOptions);
|
@ -1,16 +0,0 @@
|
||||
const growableColumns = [
|
||||
'qualityProfileId',
|
||||
'path',
|
||||
'tags'
|
||||
];
|
||||
|
||||
export default function hasGrowableColumns(columns) {
|
||||
return columns.some((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
return growableColumns.includes(name) && isVisible;
|
||||
});
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
import Column from 'Components/Table/Column';
|
||||
|
||||
const growableColumns = ['qualityProfileId', 'path', 'tags'];
|
||||
|
||||
export default function hasGrowableColumns(columns: Column[]) {
|
||||
return columns.some((column) => {
|
||||
const { name, isVisible } = column;
|
||||
|
||||
return growableColumns.includes(name) && isVisible;
|
||||
});
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
const selectTableOptions = createSelector(
|
||||
(state: AppState) => state.artistIndex.tableOptions,
|
||||
(tableOptions) => tableOptions
|
||||
);
|
||||
|
||||
export default selectTableOptions;
|
@ -0,0 +1,48 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import Artist from 'Artist/Artist';
|
||||
import { ARTIST_SEARCH, REFRESH_ARTIST } from 'Commands/commandNames';
|
||||
import createArtistMetadataProfileSelector from 'Store/Selectors/createArtistMetadataProfileSelector';
|
||||
import createArtistQualityProfileSelector from 'Store/Selectors/createArtistQualityProfileSelector';
|
||||
import { createArtistSelectorForHook } from 'Store/Selectors/createArtistSelector';
|
||||
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
|
||||
|
||||
function createArtistIndexItemSelector(artistId: number) {
|
||||
return createSelector(
|
||||
createArtistSelectorForHook(artistId),
|
||||
createArtistQualityProfileSelector(artistId),
|
||||
createArtistMetadataProfileSelector(artistId),
|
||||
createExecutingCommandsSelector(),
|
||||
(artist: Artist, qualityProfile, metadataProfile, executingCommands) => {
|
||||
// If an artist is deleted this selector may fire before the parent
|
||||
// selectors, which will result in an undefined artist, if that happens
|
||||
// we want to return early here and again in the render function to avoid
|
||||
// trying to show an artist that has no information available.
|
||||
|
||||
if (!artist) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const isRefreshingArtist = executingCommands.some((command) => {
|
||||
return (
|
||||
command.name === REFRESH_ARTIST && command.body.artistId === artist.id
|
||||
);
|
||||
});
|
||||
|
||||
const isSearchingArtist = executingCommands.some((command) => {
|
||||
return (
|
||||
command.name === ARTIST_SEARCH && command.body.artistId === artist.id
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
artist,
|
||||
qualityProfile,
|
||||
metadataProfile,
|
||||
isRefreshingArtist,
|
||||
isSearchingArtist,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default createArtistIndexItemSelector;
|
@ -1,61 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import styles from './PageContentBody.css';
|
||||
|
||||
class PageContentBody extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onScroll = (props) => {
|
||||
const { onScroll } = this.props;
|
||||
|
||||
if (this.props.onScroll && !isLocked()) {
|
||||
onScroll(props);
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
innerClassName,
|
||||
children,
|
||||
dispatch,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Scroller
|
||||
className={className}
|
||||
scrollDirection={scrollDirections.VERTICAL}
|
||||
{...otherProps}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
<div className={innerClassName}>
|
||||
{children}
|
||||
</div>
|
||||
</Scroller>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PageContentBody.propTypes = {
|
||||
className: PropTypes.string,
|
||||
innerClassName: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
onScroll: PropTypes.func,
|
||||
dispatch: PropTypes.func
|
||||
};
|
||||
|
||||
PageContentBody.defaultProps = {
|
||||
className: styles.contentBody,
|
||||
innerClassName: styles.innerContentBody
|
||||
};
|
||||
|
||||
export default PageContentBody;
|
@ -0,0 +1,51 @@
|
||||
import React, { forwardRef, ReactNode, useCallback } from 'react';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import styles from './PageContentBody.css';
|
||||
|
||||
interface PageContentBodyProps {
|
||||
className?: string;
|
||||
innerClassName?: string;
|
||||
children: ReactNode;
|
||||
initialScrollTop?: number;
|
||||
onScroll?: (payload) => void;
|
||||
}
|
||||
|
||||
const PageContentBody = forwardRef(
|
||||
(
|
||||
props: PageContentBodyProps,
|
||||
ref: React.MutableRefObject<HTMLDivElement>
|
||||
) => {
|
||||
const {
|
||||
className = styles.contentBody,
|
||||
innerClassName = styles.innerContentBody,
|
||||
children,
|
||||
onScroll,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const onScrollWrapper = useCallback(
|
||||
(payload) => {
|
||||
if (onScroll && !isLocked()) {
|
||||
onScroll(payload);
|
||||
}
|
||||
},
|
||||
[onScroll]
|
||||
);
|
||||
|
||||
return (
|
||||
<Scroller
|
||||
ref={ref}
|
||||
{...otherProps}
|
||||
className={className}
|
||||
scrollDirection={ScrollDirection.Vertical}
|
||||
onScroll={onScrollWrapper}
|
||||
>
|
||||
<div className={innerClassName}>{children}</div>
|
||||
</Scroller>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default PageContentBody;
|
@ -1,95 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import styles from './Scroller.css';
|
||||
|
||||
class Scroller extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._scroller = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
scrollDirection,
|
||||
autoFocus,
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
if (this.props.scrollTop != null) {
|
||||
this._scroller.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
if (autoFocus && scrollDirection !== scrollDirections.NONE) {
|
||||
this._scroller.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
_setScrollerRef = (ref) => {
|
||||
this._scroller = ref;
|
||||
|
||||
this.props.registerScroller(ref);
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
scrollDirection,
|
||||
autoScroll,
|
||||
children,
|
||||
scrollTop,
|
||||
onScroll,
|
||||
registerScroller,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={this._setScrollerRef}
|
||||
className={classNames(
|
||||
className,
|
||||
styles.scroller,
|
||||
styles[scrollDirection],
|
||||
autoScroll && styles.autoScroll
|
||||
)}
|
||||
tabIndex={-1}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Scroller.propTypes = {
|
||||
className: PropTypes.string,
|
||||
scrollDirection: PropTypes.oneOf(scrollDirections.all).isRequired,
|
||||
autoFocus: PropTypes.bool.isRequired,
|
||||
autoScroll: PropTypes.bool.isRequired,
|
||||
scrollTop: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
onScroll: PropTypes.func,
|
||||
registerScroller: PropTypes.func
|
||||
};
|
||||
|
||||
Scroller.defaultProps = {
|
||||
scrollDirection: scrollDirections.VERTICAL,
|
||||
autoFocus: true,
|
||||
autoScroll: true,
|
||||
registerScroller: () => { /* no-op */ }
|
||||
};
|
||||
|
||||
export default Scroller;
|
@ -0,0 +1,90 @@
|
||||
import classNames from 'classnames';
|
||||
import { throttle } from 'lodash';
|
||||
import React, { forwardRef, ReactNode, useEffect, useRef } from 'react';
|
||||
import ScrollDirection from 'Helpers/Props/ScrollDirection';
|
||||
import styles from './Scroller.css';
|
||||
|
||||
interface ScrollerProps {
|
||||
className?: string;
|
||||
scrollDirection?: ScrollDirection;
|
||||
autoFocus?: boolean;
|
||||
autoScroll?: boolean;
|
||||
scrollTop?: number;
|
||||
initialScrollTop?: number;
|
||||
children?: ReactNode;
|
||||
onScroll?: (payload) => void;
|
||||
}
|
||||
|
||||
const Scroller = forwardRef(
|
||||
(props: ScrollerProps, ref: React.MutableRefObject<HTMLDivElement>) => {
|
||||
const {
|
||||
className,
|
||||
autoFocus = false,
|
||||
autoScroll = true,
|
||||
scrollDirection = ScrollDirection.Vertical,
|
||||
children,
|
||||
scrollTop,
|
||||
initialScrollTop,
|
||||
onScroll,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const internalRef = useRef();
|
||||
const currentRef = ref ?? internalRef;
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (initialScrollTop != null) {
|
||||
currentRef.current.scrollTop = initialScrollTop;
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollTop != null) {
|
||||
currentRef.current.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
if (autoFocus && scrollDirection !== ScrollDirection.None) {
|
||||
currentRef.current.focus({ preventScroll: true });
|
||||
}
|
||||
}, [autoFocus, currentRef, scrollDirection, scrollTop]);
|
||||
|
||||
useEffect(() => {
|
||||
const div = currentRef.current;
|
||||
|
||||
const handleScroll = throttle(() => {
|
||||
const scrollLeft = div.scrollLeft;
|
||||
const scrollTop = div.scrollTop;
|
||||
|
||||
onScroll?.({ scrollLeft, scrollTop });
|
||||
}, 10);
|
||||
|
||||
div.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
div.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, [currentRef, onScroll]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...otherProps}
|
||||
ref={currentRef}
|
||||
className={classNames(
|
||||
className,
|
||||
styles.scroller,
|
||||
styles[scrollDirection],
|
||||
autoScroll && styles.autoScroll
|
||||
)}
|
||||
tabIndex={-1}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default Scroller;
|
@ -1,30 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
|
||||
function withScrollPosition(WrappedComponent, scrollPositionKey) {
|
||||
function ScrollPosition(props) {
|
||||
const {
|
||||
history
|
||||
} = props;
|
||||
|
||||
const scrollTop = history.action === 'POP' || (history.location.state && history.location.state.restoreScrollPosition) ?
|
||||
scrollPositions[scrollPositionKey] :
|
||||
0;
|
||||
|
||||
return (
|
||||
<WrappedComponent
|
||||
{...props}
|
||||
scrollTop={scrollTop}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ScrollPosition.propTypes = {
|
||||
history: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
return ScrollPosition;
|
||||
}
|
||||
|
||||
export default withScrollPosition;
|
@ -0,0 +1,25 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
|
||||
function withScrollPosition(WrappedComponent, scrollPositionKey) {
|
||||
function ScrollPosition(props) {
|
||||
const { history } = props;
|
||||
|
||||
const initialScrollTop =
|
||||
history.action === 'POP' ||
|
||||
(history.location.state && history.location.state.restoreScrollPosition)
|
||||
? scrollPositions[scrollPositionKey]
|
||||
: 0;
|
||||
|
||||
return <WrappedComponent {...props} initialScrollTop={initialScrollTop} />;
|
||||
}
|
||||
|
||||
ScrollPosition.propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
return ScrollPosition;
|
||||
}
|
||||
|
||||
export default withScrollPosition;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue