mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 09:09:29 +01:00
build out artist view
clean up mapping methods a bit
This commit is contained in:
parent
e7f9b1db86
commit
62a721ba4d
@ -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>
|
||||
)
|
||||
})
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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',
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)),
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user