Browse Source
- add artist view/route - add album view/route - add links from other components to artist/album routes - separate viewtype settings by type - add list types for albumsmaster
jeffvli
3 years ago
20 changed files with 930 additions and 160 deletions
@ -0,0 +1,161 @@ |
|||||
|
import React, { useState } from 'react'; |
||||
|
import settings from 'electron-settings'; |
||||
|
import { ButtonToolbar, Tag } from 'rsuite'; |
||||
|
import { useQuery } from 'react-query'; |
||||
|
import { useParams, useHistory } from 'react-router-dom'; |
||||
|
import { |
||||
|
DeleteButton, |
||||
|
EditButton, |
||||
|
PlayAppendButton, |
||||
|
PlayButton, |
||||
|
PlayShuffleAppendButton, |
||||
|
PlayShuffleButton, |
||||
|
} from '../shared/ToolbarButtons'; |
||||
|
import { getAlbum } from '../../api/api'; |
||||
|
import { useAppDispatch } from '../../redux/hooks'; |
||||
|
import { fixPlayer2Index, setPlayQueue } from '../../redux/playQueueSlice'; |
||||
|
import { |
||||
|
toggleSelected, |
||||
|
setRangeSelected, |
||||
|
toggleRangeSelected, |
||||
|
setSelected, |
||||
|
clearSelected, |
||||
|
} from '../../redux/multiSelectSlice'; |
||||
|
import useSearchQuery from '../../hooks/useSearchQuery'; |
||||
|
import GenericPage from '../layout/GenericPage'; |
||||
|
import ListViewType from '../viewtypes/ListViewType'; |
||||
|
import Loader from '../loader/Loader'; |
||||
|
import GenericPageHeader from '../layout/GenericPageHeader'; |
||||
|
import { TagLink } from './styled'; |
||||
|
|
||||
|
interface AlbumParams { |
||||
|
id: string; |
||||
|
} |
||||
|
|
||||
|
const AlbumView = () => { |
||||
|
const dispatch = useAppDispatch(); |
||||
|
const history = useHistory(); |
||||
|
const { id } = useParams<AlbumParams>(); |
||||
|
const { isLoading, isError, data, error }: any = useQuery(['album', id], () => |
||||
|
getAlbum(id) |
||||
|
); |
||||
|
const [searchQuery, setSearchQuery] = useState(''); |
||||
|
const filteredData = useSearchQuery(searchQuery, data?.song, [ |
||||
|
'title', |
||||
|
'artist', |
||||
|
'album', |
||||
|
'genre', |
||||
|
]); |
||||
|
|
||||
|
let timeout: any = null; |
||||
|
const handleRowClick = (e: any, rowData: any) => { |
||||
|
if (timeout === null) { |
||||
|
timeout = window.setTimeout(() => { |
||||
|
timeout = null; |
||||
|
|
||||
|
if (e.ctrlKey) { |
||||
|
dispatch(toggleSelected(rowData)); |
||||
|
} else if (e.shiftKey) { |
||||
|
dispatch(setRangeSelected(rowData)); |
||||
|
|
||||
|
dispatch(toggleRangeSelected(data.song)); |
||||
|
} else { |
||||
|
dispatch(setSelected(rowData)); |
||||
|
} |
||||
|
}, 300); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handleRowDoubleClick = (e: any) => { |
||||
|
window.clearTimeout(timeout); |
||||
|
timeout = null; |
||||
|
const newPlayQueue = data.song.slice([e.index], data.song.length); |
||||
|
|
||||
|
dispatch(clearSelected()); |
||||
|
dispatch(setPlayQueue(newPlayQueue)); |
||||
|
dispatch(fixPlayer2Index()); |
||||
|
}; |
||||
|
|
||||
|
if (isLoading) { |
||||
|
return <Loader />; |
||||
|
} |
||||
|
|
||||
|
if (isError) { |
||||
|
return <span>Error: {error.message}</span>; |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<GenericPage |
||||
|
header={ |
||||
|
<GenericPageHeader |
||||
|
image={data.image} |
||||
|
title={data.name} |
||||
|
subtitle={ |
||||
|
<div> |
||||
|
<div |
||||
|
style={{ |
||||
|
overflow: 'hidden', |
||||
|
textOverflow: 'ellipsis', |
||||
|
whiteSpace: 'nowrap', |
||||
|
}} |
||||
|
> |
||||
|
{data.artist && ( |
||||
|
<Tag> |
||||
|
<TagLink |
||||
|
onClick={() => |
||||
|
history.push(`/library/artist/${data.artistId}`) |
||||
|
} |
||||
|
> |
||||
|
Artist: {data.artist} |
||||
|
</TagLink> |
||||
|
</Tag> |
||||
|
)} |
||||
|
{data.year && ( |
||||
|
<Tag> |
||||
|
<TagLink>Year: {data.year}</TagLink> |
||||
|
</Tag> |
||||
|
)} |
||||
|
{data.genre && ( |
||||
|
<Tag> |
||||
|
<TagLink>Genre: {data.genre}</TagLink> |
||||
|
</Tag> |
||||
|
)} |
||||
|
</div> |
||||
|
<div style={{ marginTop: '10px' }}> |
||||
|
<ButtonToolbar> |
||||
|
<PlayButton appearance="primary" size="lg" circle /> |
||||
|
<PlayShuffleButton /> |
||||
|
<PlayAppendButton /> |
||||
|
<PlayShuffleAppendButton /> |
||||
|
<EditButton /> |
||||
|
<DeleteButton /> |
||||
|
</ButtonToolbar> |
||||
|
</div> |
||||
|
</div> |
||||
|
} |
||||
|
searchQuery={searchQuery} |
||||
|
handleSearch={(e: any) => setSearchQuery(e)} |
||||
|
clearSearchQuery={() => setSearchQuery('')} |
||||
|
showSearchBar |
||||
|
/> |
||||
|
} |
||||
|
> |
||||
|
<ListViewType |
||||
|
data={searchQuery !== '' ? filteredData : data.song} |
||||
|
tableColumns={settings.getSync('songListColumns')} |
||||
|
handleRowClick={handleRowClick} |
||||
|
handleRowDoubleClick={handleRowDoubleClick} |
||||
|
tableHeight={700} |
||||
|
virtualized |
||||
|
rowHeight={Number(settings.getSync('songListRowHeight'))} |
||||
|
fontSize={Number(settings.getSync('songListFontSize'))} |
||||
|
cacheImages={{ |
||||
|
enabled: settings.getSync('cacheImages'), |
||||
|
cacheType: 'album', |
||||
|
}} |
||||
|
/> |
||||
|
</GenericPage> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default AlbumView; |
@ -0,0 +1,212 @@ |
|||||
|
import React, { useState } from 'react'; |
||||
|
import settings from 'electron-settings'; |
||||
|
import { ButtonToolbar, Tag, Whisper, Button, Popover, TagGroup } from 'rsuite'; |
||||
|
import { useQuery } from 'react-query'; |
||||
|
import { useParams, useHistory } from 'react-router-dom'; |
||||
|
import { |
||||
|
PlayAppendButton, |
||||
|
PlayButton, |
||||
|
PlayShuffleAppendButton, |
||||
|
PlayShuffleButton, |
||||
|
EditButton, |
||||
|
} from '../shared/ToolbarButtons'; |
||||
|
import { getArtist, getArtistInfo } from '../../api/api'; |
||||
|
import { useAppDispatch } from '../../redux/hooks'; |
||||
|
import { fixPlayer2Index, setPlayQueue } from '../../redux/playQueueSlice'; |
||||
|
import { |
||||
|
toggleSelected, |
||||
|
setRangeSelected, |
||||
|
toggleRangeSelected, |
||||
|
setSelected, |
||||
|
clearSelected, |
||||
|
} from '../../redux/multiSelectSlice'; |
||||
|
import useSearchQuery from '../../hooks/useSearchQuery'; |
||||
|
import GenericPage from '../layout/GenericPage'; |
||||
|
import ListViewType from '../viewtypes/ListViewType'; |
||||
|
import GridViewType from '../viewtypes/GridViewType'; |
||||
|
import Loader from '../loader/Loader'; |
||||
|
import GenericPageHeader from '../layout/GenericPageHeader'; |
||||
|
import CustomTooltip from '../shared/CustomTooltip'; |
||||
|
import { TagLink } from './styled'; |
||||
|
|
||||
|
interface ArtistParams { |
||||
|
id: string; |
||||
|
} |
||||
|
|
||||
|
const ArtistView = () => { |
||||
|
const dispatch = useAppDispatch(); |
||||
|
const history = useHistory(); |
||||
|
const [viewType, setViewType] = useState( |
||||
|
settings.getSync('albumViewType') || 'list' |
||||
|
); |
||||
|
const { id } = useParams<ArtistParams>(); |
||||
|
const { isLoading, isError, data, error }: any = useQuery( |
||||
|
['artist', id], |
||||
|
() => getArtist(id) |
||||
|
); |
||||
|
const { |
||||
|
isLoading: isLoadingAI, |
||||
|
isError: isErrorAI, |
||||
|
data: artistInfo, |
||||
|
error: errorAI, |
||||
|
}: any = useQuery(['artistInfo', id], () => getArtistInfo(id, 8)); |
||||
|
|
||||
|
const [searchQuery, setSearchQuery] = useState(''); |
||||
|
const filteredData = useSearchQuery(searchQuery, data?.song, [ |
||||
|
'title', |
||||
|
'artist', |
||||
|
'album', |
||||
|
'genre', |
||||
|
]); |
||||
|
|
||||
|
let timeout: any = null; |
||||
|
const handleRowClick = (e: any, rowData: any) => { |
||||
|
if (timeout === null) { |
||||
|
timeout = window.setTimeout(() => { |
||||
|
timeout = null; |
||||
|
|
||||
|
if (e.ctrlKey) { |
||||
|
dispatch(toggleSelected(rowData)); |
||||
|
} else if (e.shiftKey) { |
||||
|
dispatch(setRangeSelected(rowData)); |
||||
|
|
||||
|
dispatch(toggleRangeSelected(data.album)); |
||||
|
} else { |
||||
|
dispatch(setSelected(rowData)); |
||||
|
} |
||||
|
}, 300); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const handleRowDoubleClick = (e: any) => { |
||||
|
window.clearTimeout(timeout); |
||||
|
timeout = null; |
||||
|
const newPlayQueue = data.album.slice([e.index], data.album.length); |
||||
|
|
||||
|
dispatch(clearSelected()); |
||||
|
dispatch(setPlayQueue(newPlayQueue)); |
||||
|
dispatch(fixPlayer2Index()); |
||||
|
}; |
||||
|
|
||||
|
if (isLoading || isLoadingAI) { |
||||
|
return <Loader />; |
||||
|
} |
||||
|
|
||||
|
if (isError || isErrorAI) { |
||||
|
return ( |
||||
|
<span> |
||||
|
Error: {error.message} {errorAI.message} |
||||
|
</span> |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return ( |
||||
|
<GenericPage |
||||
|
header={ |
||||
|
<GenericPageHeader |
||||
|
image={data.image} |
||||
|
imageHeight={145} |
||||
|
title={data.name} |
||||
|
subtitle={ |
||||
|
<> |
||||
|
<CustomTooltip |
||||
|
text={artistInfo.biography |
||||
|
?.replace(/<[^>]*>/, '') |
||||
|
.replace('Read more on Last.fm</a>', '')} |
||||
|
placement="bottomStart" |
||||
|
> |
||||
|
<span> |
||||
|
{artistInfo.biography |
||||
|
?.replace(/<[^>]*>/, '') |
||||
|
.replace('Read more on Last.fm</a>', '') !== '' |
||||
|
? `${artistInfo.biography |
||||
|
?.replace(/<[^>]*>/, '') |
||||
|
.replace('Read more on Last.fm</a>', '')}` |
||||
|
: 'No artist biography found'} |
||||
|
</span> |
||||
|
</CustomTooltip> |
||||
|
<div style={{ marginTop: '10px' }}> |
||||
|
<ButtonToolbar> |
||||
|
<PlayButton appearance="primary" size="lg" circle /> |
||||
|
<PlayShuffleButton /> |
||||
|
<PlayAppendButton /> |
||||
|
<PlayShuffleAppendButton /> |
||||
|
<EditButton style={{ marginRight: '10px' }} /> |
||||
|
<Whisper |
||||
|
placement="bottomStart" |
||||
|
trigger="click" |
||||
|
speaker={ |
||||
|
<Popover style={{ width: '400px' }}> |
||||
|
<TagGroup> |
||||
|
{artistInfo.similarArtist?.map((artist: any) => ( |
||||
|
<Tag key={artist.id}> |
||||
|
<TagLink |
||||
|
onClick={() => |
||||
|
history.push(`/library/artist/${artist.id}`) |
||||
|
} |
||||
|
> |
||||
|
{artist.name} |
||||
|
</TagLink> |
||||
|
</Tag> |
||||
|
))} |
||||
|
</TagGroup> |
||||
|
</Popover> |
||||
|
} |
||||
|
> |
||||
|
<Button>Related Artists</Button> |
||||
|
</Whisper> |
||||
|
</ButtonToolbar> |
||||
|
</div> |
||||
|
</> |
||||
|
} |
||||
|
searchQuery={searchQuery} |
||||
|
handleSearch={(e: any) => setSearchQuery(e)} |
||||
|
clearSearchQuery={() => setSearchQuery('')} |
||||
|
showSearchBar |
||||
|
showViewTypeButtons |
||||
|
viewTypeSetting="album" |
||||
|
handleListClick={() => setViewType('list')} |
||||
|
handleGridClick={() => setViewType('grid')} |
||||
|
/> |
||||
|
} |
||||
|
> |
||||
|
<> |
||||
|
{viewType === 'list' && ( |
||||
|
<ListViewType |
||||
|
data={searchQuery !== '' ? filteredData : data.album} |
||||
|
tableColumns={settings.getSync('albumListColumns')} |
||||
|
handleRowClick={handleRowClick} |
||||
|
handleRowDoubleClick={handleRowDoubleClick} |
||||
|
virtualized |
||||
|
rowHeight={Number(settings.getSync('albumListRowHeight'))} |
||||
|
fontSize={Number(settings.getSync('albumListFontSize'))} |
||||
|
cacheImages={{ |
||||
|
enabled: settings.getSync('cacheImages'), |
||||
|
cacheType: 'album', |
||||
|
}} |
||||
|
/> |
||||
|
)} |
||||
|
|
||||
|
{viewType === 'grid' && ( |
||||
|
<GridViewType |
||||
|
data={searchQuery === '' ? data.album : filteredData} |
||||
|
cardTitle={{ |
||||
|
prefix: '/library/album', |
||||
|
property: 'name', |
||||
|
urlProperty: 'albumId', |
||||
|
}} |
||||
|
cardSubtitle={{ |
||||
|
property: 'songCount', |
||||
|
unit: ' tracks', |
||||
|
}} |
||||
|
playClick={{ type: 'album', idProperty: 'id' }} |
||||
|
size="150px" |
||||
|
cacheType="album" |
||||
|
/> |
||||
|
)} |
||||
|
</> |
||||
|
</GenericPage> |
||||
|
); |
||||
|
}; |
||||
|
|
||||
|
export default ArtistView; |
@ -0,0 +1,11 @@ |
|||||
|
import styled from 'styled-components'; |
||||
|
|
||||
|
export const TagLink = styled.a` |
||||
|
color: #e9ebf0; |
||||
|
cursor: pointer; |
||||
|
|
||||
|
&:hover { |
||||
|
color: #e9ebf0; |
||||
|
text-decoration: none; |
||||
|
} |
||||
|
`;
|
Loading…
Reference in new issue