build out artist view

clean up mapping methods a bit
This commit is contained in:
austinried 2021-07-15 16:58:08 +09:00
parent e7f9b1db86
commit 62a721ba4d
9 changed files with 180 additions and 83 deletions

View File

@ -164,7 +164,13 @@ const ArtistArt = React.memo<ArtistArtProps>(({ id, height, width }) => {
return (
<View style={[styles.container, { borderRadius: height / 2 }]}>
<CoverArt PlaceholderComponent={Placeholder} height={height} width={width} coverArtUri={artistArt?.uri} />
<CoverArt
PlaceholderComponent={Placeholder}
height={height}
width={width}
coverArtUri={artistArt?.uri}
resizeMode={FastImage.resizeMode.cover}
/>
</View>
)
})

View File

@ -11,7 +11,8 @@ const CoverArt: React.FC<{
height?: string | number
width?: string | number
coverArtUri?: string
}> = ({ PlaceholderComponent, placeholderIcon, height, width, coverArtUri }) => {
resizeMode?: keyof typeof FastImage.resizeMode
}> = ({ PlaceholderComponent, placeholderIcon, height, width, coverArtUri, resizeMode }) => {
const [placeholderVisible, setPlaceholderVisible] = useState(false)
const [loading, setLoading] = useState(true)
const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 })
@ -26,7 +27,7 @@ const CoverArt: React.FC<{
<FastImage
source={{ uri: coverArtUri, priority: 'high' }}
style={{ ...styles.image, opacity: placeholderVisible ? 0 : 1 }}
resizeMode={FastImage.resizeMode.contain}
resizeMode={resizeMode || FastImage.resizeMode.contain}
onError={() => {
setLoading(false)
setPlaceholderVisible(true)

View File

@ -17,12 +17,14 @@ const GradientBackground: React.FC<{
<LinearGradient
colors={colors || [colorStyles.gradient.high, colorStyles.gradient.low]}
locations={locations || [0.01, 0.7]}
style={{
...style,
width: width || '100%',
height: height || layout.height,
position: position || 'absolute',
}}>
style={[
style,
{
width: width || '100%',
height: height || layout.height,
position: position || 'absolute',
},
]}>
{children}
</LinearGradient>
)

View File

@ -4,7 +4,11 @@ import dimensions from '@app/styles/dimensions'
import React from 'react'
import { ScrollView, ScrollViewProps, useWindowDimensions } from 'react-native'
const GradientScrollView: React.FC<ScrollViewProps> = props => {
const GradientScrollView: React.FC<
ScrollViewProps & {
offset?: number
}
> = props => {
const layout = useWindowDimensions()
const minHeight = layout.height - (dimensions.top() + dimensions.bottom())
@ -15,7 +19,7 @@ const GradientScrollView: React.FC<ScrollViewProps> = props => {
{...props}
style={[props.style, { backgroundColor: colors.gradient.low }]}
contentContainerStyle={[props.contentContainerStyle, { minHeight }]}>
<GradientBackground />
<GradientBackground style={{ top: props.offset }} />
{props.children}
</ScrollView>
)

View File

@ -17,18 +17,6 @@ export interface ArtistArt {
coverArtUris: string[]
}
export interface Album {
id: string
artistId?: string
artist?: string
name: string
starred?: Date
coverArt?: string
coverArtUri?: string
coverArtThumbUri?: string
year?: number
}
export interface AlbumListItem {
id: string
name: string
@ -37,6 +25,11 @@ export interface AlbumListItem {
coverArtThumbUri?: string
}
export interface Album extends AlbumListItem {
coverArtUri?: string
year?: number
}
export interface AlbumWithSongs extends Album {
songs: Song[]
}
@ -47,19 +40,7 @@ export interface Song {
artist?: string
title: string
track?: number
year?: number
genre?: string
coverArt?: string
size?: number
contentType?: string
suffix?: string
duration?: number
bitRate?: number
userRating?: number
averageRating?: number
playCount?: number
discNumber?: number
created?: Date
starred?: Date
streamUri: string

View File

@ -19,13 +19,6 @@ type TabStackParamList = {
ArtistView: { id: string; title: string }
}
type TabMainScreenNavigationProp = NativeStackNavigationProp<TabStackParamList, 'TabMain'>
type TabMainScreenRouteProp = RouteProp<TabStackParamList, 'TabMain'>
type TabMainScreenProps = {
route: TabMainScreenRouteProp
navigation: TabMainScreenNavigationProp
}
type AlbumScreenNavigationProp = NativeStackNavigationProp<TabStackParamList, 'AlbumView'>
type AlbumScreenRouteProp = RouteProp<TabStackParamList, 'AlbumView'>
type AlbumScreenProps = {

View File

@ -1,24 +1,64 @@
import CoverArt from '@app/components/CoverArt'
import GradientScrollView from '@app/components/GradientScrollView'
import PressableOpacity from '@app/components/PressableOpacity'
import { Album } from '@app/models/music'
import { artistInfoAtomFamily } from '@app/state/music'
import colors from '@app/styles/colors'
import font from '@app/styles/font'
import { useLayout } from '@react-native-community/hooks'
import { useNavigation } from '@react-navigation/native'
import { useAtomValue } from 'jotai/utils'
import React, { useEffect } from 'react'
import { StyleSheet, Text } from 'react-native'
import { artistInfoAtomFamily } from '@app/state/music'
import ArtistArt from '@app/components/ArtistArt'
import GradientScrollView from '@app/components/GradientScrollView'
import font from '@app/styles/font'
import colors from '@app/styles/colors'
import { StyleSheet, Text, View } from 'react-native'
import FastImage from 'react-native-fast-image'
const AlbumItem = React.memo<{
album: Album
height: number
width: number
}>(({ album, height, width }) => {
const navigation = useNavigation()
return (
<PressableOpacity
onPress={() => navigation.navigate('AlbumView', { id: album.id, title: album.name })}
key={album.id}
style={[styles.albumItem, { width }]}>
<CoverArt coverArtUri={album.coverArtThumbUri} height={height} width={width} />
<Text style={styles.albumTitle}>{album.name}</Text>
<Text style={styles.albumYear}> {album.year ? album.year : ''}</Text>
</PressableOpacity>
)
})
const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
const artist = useAtomValue(artistInfoAtomFamily(id))
const layout = useLayout()
const size = layout.width / 2 - styles.container.paddingHorizontal / 2
if (!artist) {
return <></>
}
return (
<GradientScrollView style={styles.scroll} contentContainerStyle={styles.scrollContent}>
<Text style={styles.title}>{artist.name}</Text>
<ArtistArt id={artist.id} height={200} width={200} />
<GradientScrollView offset={artistImageHeight} style={styles.scroll} contentContainerStyle={styles.scrollContent}>
<FastImage
style={[styles.artistImage]}
source={{ uri: artist.largeImageUrl }}
resizeMode={FastImage.resizeMode.cover}
/>
<View style={styles.titleContainer}>
<Text style={styles.title}>{artist.name}</Text>
</View>
<View style={styles.container}>
<Text style={styles.header}>Albums</Text>
<View style={styles.albums} onLayout={layout.onLayout}>
{artist.albums.map(a => (
<AlbumItem key={a.id} album={a} height={size} width={size} />
))}
</View>
</View>
</GradientScrollView>
)
}
@ -40,6 +80,8 @@ const ArtistView: React.FC<{
)
}
const artistImageHeight = 280
const styles = StyleSheet.create({
scroll: {
flex: 1,
@ -47,10 +89,58 @@ const styles = StyleSheet.create({
scrollContent: {
alignItems: 'center',
},
container: {
width: '100%',
paddingHorizontal: 20,
},
titleContainer: {
width: '100%',
height: artistImageHeight,
justifyContent: 'flex-end',
},
title: {
fontFamily: font.regular,
fontSize: 16,
fontFamily: font.bold,
fontSize: 44,
color: colors.text.primary,
textAlign: 'left',
textShadowColor: 'black',
textShadowOffset: { width: 0, height: 0 },
textShadowRadius: 16,
paddingHorizontal: 10,
},
header: {
fontFamily: font.bold,
fontSize: 24,
color: colors.text.primary,
marginTop: 14,
},
artistImage: {
position: 'absolute',
width: '100%',
height: artistImageHeight,
},
albums: {
marginTop: 14,
width: '100%',
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'flex-start',
justifyContent: 'space-between',
},
albumItem: {
marginBottom: 20,
},
albumTitle: {
fontFamily: font.semiBold,
fontSize: 14,
color: colors.text.primary,
marginTop: 4,
textAlign: 'center',
},
albumYear: {
color: colors.text.secondary,
fontFamily: font.regular,
textAlign: 'center',
},
})

View File

@ -38,7 +38,7 @@ const Category = React.memo<{
}>(({ name, data }) => {
return (
<View style={styles.category}>
<Text style={styles.header}>{name}</Text>
<Text style={styles.categoryHeader}>{name}</Text>
<ScrollView
horizontal={true}
showsHorizontalScrollIndicator={false}
@ -95,7 +95,7 @@ const styles = StyleSheet.create({
category: {
marginTop: 12,
},
header: {
categoryHeader: {
fontFamily: font.bold,
fontSize: 24,
color: colors.text.primary,

View File

@ -1,7 +1,7 @@
import { Album, AlbumListItem, AlbumWithSongs, Artist, ArtistArt, ArtistInfo, Song } from '@app/models/music'
import { activeServerAtom, homeListTypesAtom } from '@app/state/settings'
import { SubsonicApiClient } from '@app/subsonic/api'
import { AlbumID3Element, ArtistInfo2Element, ChildElement } from '@app/subsonic/elements'
import { AlbumID3Element, ArtistID3Element, ArtistInfo2Element, ChildElement } from '@app/subsonic/elements'
import { GetAlbumList2Type } from '@app/subsonic/params'
import { GetArtistResponse } from '@app/subsonic/responses'
import { atom, useAtom } from 'jotai'
@ -28,13 +28,7 @@ export const useUpdateArtists = () => {
const client = new SubsonicApiClient(server)
const response = await client.getArtists()
setArtists(
response.data.artists.map(x => ({
id: x.id,
name: x.name,
starred: x.starred,
})),
)
setArtists(response.data.artists.map(mapArtistID3toArtist))
setUpdating(false)
}
}
@ -121,7 +115,7 @@ export const albumAtomFamily = atomFamily((id: string) =>
const client = new SubsonicApiClient(server)
const response = await client.getAlbum({ id })
return mapAlbumID3WithSongs(response.data.album, response.data.songs, client)
return mapAlbumID3WithSongstoAlbunWithSongs(response.data.album, response.data.songs, client)
}),
)
@ -161,45 +155,56 @@ export const artistArtAtomFamily = atomFamily((id: string) =>
return {
coverArtUris,
uri: artistInfo.mediumImageUrl,
uri: artistInfo.largeImageUrl,
}
}),
)
function mapArtistID3toArtist(artist: ArtistID3Element): Artist {
return {
id: artist.id,
name: artist.name,
starred: artist.starred,
}
}
function mapArtistInfo(
artistResponse: GetArtistResponse,
artistInfo: ArtistInfo2Element,
info: ArtistInfo2Element,
client: SubsonicApiClient,
): ArtistInfo {
const info = { ...artistInfo } as any
delete info.similarArtists
const { artist, albums } = artistResponse
const mappedAlbums = albums.map(a => mapAlbumID3(a, client))
const mappedAlbums = albums.map(a => mapAlbumID3toAlbum(a, client))
const coverArtUris = mappedAlbums
.sort((a, b) => {
if (a.year && b.year) {
return a.year - b.year
return b.year - a.year
} else {
return a.name.localeCompare(b.name) - 9000
}
})
.map(a => a.coverArtThumbUri)
.filter(a => a !== undefined) as string[]
return {
...artist,
...info,
...mapArtistID3toArtist(artist),
albums: mappedAlbums,
coverArtUris,
mediumImageUrl: info.mediumImageUrl,
largeImageUrl: info.largeImageUrl,
}
}
function mapAlbumID3(album: AlbumID3Element, client: SubsonicApiClient): Album {
function mapCoverArtUri(item: { coverArt?: string }, client: SubsonicApiClient) {
return {
...album,
coverArtUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt }) : undefined,
coverArtThumbUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt, size: '256' }) : undefined,
coverArtUri: item.coverArt ? client.getCoverArtUri({ id: item.coverArt }) : undefined,
}
}
function mapCoverArtThumbUri(item: { coverArt?: string }, client: SubsonicApiClient) {
return {
coverArtThumbUri: item.coverArt ? client.getCoverArtUri({ id: item.coverArt, size: '256' }) : undefined,
}
}
@ -209,26 +214,41 @@ function mapAlbumID3toAlbumListItem(album: AlbumID3Element, client: SubsonicApiC
name: album.name,
artist: album.artist,
starred: album.starred,
coverArtThumbUri: album.coverArt ? client.getCoverArtUri({ id: album.coverArt, size: '256' }) : undefined,
...mapCoverArtThumbUri(album, client),
}
}
function mapAlbumID3toAlbum(album: AlbumID3Element, client: SubsonicApiClient): Album {
return {
...mapAlbumID3toAlbumListItem(album, client),
...mapCoverArtUri(album, client),
...mapCoverArtThumbUri(album, client),
year: album.year,
}
}
function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
return {
...child,
id: child.id,
album: child.album,
artist: child.artist,
title: child.title,
track: child.track,
duration: child.duration,
starred: child.starred,
streamUri: client.streamUri({ id: child.id }),
coverArtUri: child.coverArt ? client.getCoverArtUri({ id: child.coverArt }) : undefined,
coverArtThumbUri: child.coverArt ? client.getCoverArtUri({ id: child.coverArt, size: '256' }) : undefined,
...mapCoverArtUri(child, client),
...mapCoverArtThumbUri(child, client),
}
}
function mapAlbumID3WithSongs(
function mapAlbumID3WithSongstoAlbunWithSongs(
album: AlbumID3Element,
songs: ChildElement[],
client: SubsonicApiClient,
): AlbumWithSongs {
return {
...mapAlbumID3(album, client),
...mapAlbumID3toAlbum(album, client),
songs: songs.map(s => mapChildToSong(s, client)),
}
}