From de342c083086c779f6793495e3f9cc7225d8df2a Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sat, 17 Jul 2021 10:39:18 +0900 Subject: [PATCH] added top songs to artist view --- app/components/ArtistArt.tsx | 50 +++++++++---------- app/components/CoverArt.tsx | 7 +-- app/components/SongItem.tsx | 84 ++++++++++++++++++++++++++++++++ app/models/music.ts | 6 ++- app/screens/AlbumView.tsx | 93 +++++------------------------------- app/screens/ArtistView.tsx | 10 ++-- app/state/music.ts | 14 ++++-- app/subsonic/api.ts | 7 +++ app/subsonic/params.ts | 5 ++ app/subsonic/responses.ts | 11 +++++ 10 files changed, 168 insertions(+), 119 deletions(-) create mode 100644 app/components/SongItem.tsx diff --git a/app/components/ArtistArt.tsx b/app/components/ArtistArt.tsx index ec039ba..31287f7 100644 --- a/app/components/ArtistArt.tsx +++ b/app/components/ArtistArt.tsx @@ -14,7 +14,7 @@ interface ArtistArtSizeProps { } interface ArtistArtXUpProps extends ArtistArtSizeProps { - coverArtUris: string[] + albumCoverUris: string[] } interface ArtistArtProps extends ArtistArtSizeProps { @@ -39,7 +39,7 @@ const PlaceholderContainer: React.FC = ({ height, width, chi ) } -const FourUp = React.memo(({ height, width, coverArtUris }) => { +const FourUp = React.memo(({ height, width, albumCoverUris }) => { const halfHeight = height / 2 const halfWidth = width / 2 @@ -47,24 +47,24 @@ const FourUp = React.memo(({ height, width, coverArtUris }) = @@ -73,7 +73,7 @@ const FourUp = React.memo(({ height, width, coverArtUris }) = ) }) -const ThreeUp = React.memo(({ height, width, coverArtUris }) => { +const ThreeUp = React.memo(({ height, width, albumCoverUris }) => { const halfHeight = height / 2 const halfWidth = width / 2 @@ -81,19 +81,19 @@ const ThreeUp = React.memo(({ height, width, coverArtUris }) @@ -102,21 +102,21 @@ const ThreeUp = React.memo(({ height, width, coverArtUris }) ) }) -const TwoUp = React.memo(({ height, width, coverArtUris }) => { +const TwoUp = React.memo(({ height, width, albumCoverUris }) => { const halfHeight = height / 2 return ( @@ -125,9 +125,9 @@ const TwoUp = React.memo(({ height, width, coverArtUris }) => ) }) -const OneUp = React.memo(({ height, width, coverArtUris }) => ( +const OneUp = React.memo(({ height, width, albumCoverUris }) => ( - + )) @@ -141,22 +141,22 @@ const ArtistArt = React.memo(({ id, height, width }) => { const Placeholder = () => { const none = - if (!artistArt || !artistArt.coverArtUris) { + if (!artistArt || !artistArt.albumCoverUris) { return none } - const { coverArtUris } = artistArt + const { albumCoverUris } = artistArt - if (coverArtUris.length >= 4) { - return + if (albumCoverUris.length >= 4) { + return } - if (coverArtUris.length === 3) { - return + if (albumCoverUris.length === 3) { + return } - if (coverArtUris.length === 2) { - return + if (albumCoverUris.length === 2) { + return } - if (coverArtUris.length === 1) { - return + if (albumCoverUris.length === 1) { + return } return none diff --git a/app/components/CoverArt.tsx b/app/components/CoverArt.tsx index ff2f8ce..21d9a2c 100644 --- a/app/components/CoverArt.tsx +++ b/app/components/CoverArt.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react' -import { ActivityIndicator, LayoutChangeEvent, StyleSheet, View } from 'react-native' +import { ActivityIndicator, LayoutChangeEvent, StyleSheet, View, ViewStyle } from 'react-native' import FastImage from 'react-native-fast-image' import colors from '@app/styles/colors' import IconFA5 from 'react-native-vector-icons/FontAwesome5' @@ -12,7 +12,8 @@ const CoverArt: React.FC<{ width?: string | number coverArtUri?: string resizeMode?: keyof typeof FastImage.resizeMode -}> = ({ PlaceholderComponent, placeholderIcon, height, width, coverArtUri, resizeMode }) => { + style?: ViewStyle +}> = ({ PlaceholderComponent, placeholderIcon, height, width, coverArtUri, resizeMode, style }) => { const [placeholderVisible, setPlaceholderVisible] = useState(false) const [loading, setLoading] = useState(true) const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 }) @@ -47,7 +48,7 @@ const CoverArt: React.FC<{ } return ( - + {coverArtUri ? : <>} {PlaceholderComponent ? : } diff --git a/app/components/SongItem.tsx b/app/components/SongItem.tsx new file mode 100644 index 0000000..f9900b3 --- /dev/null +++ b/app/components/SongItem.tsx @@ -0,0 +1,84 @@ +import { Song } from '@app/models/music' +import { currentTrackAtom } from '@app/state/trackplayer' +import colors from '@app/styles/colors' +import font from '@app/styles/font' +import { useAtomValue } from 'jotai/utils' +import React from 'react' +import { GestureResponderEvent, StyleSheet, Text, View } from 'react-native' +import IconFA from 'react-native-vector-icons/FontAwesome' +import IconMat from 'react-native-vector-icons/MaterialIcons' +import CoverArt from './CoverArt' +import PressableOpacity from './PressableOpacity' + +const SongItem: React.FC<{ + song: Song + onPress?: (event: GestureResponderEvent) => void + showArt?: boolean + subtitle?: 'artist' | 'album' +}> = ({ song, onPress, showArt, subtitle }) => { + const currentTrack = useAtomValue(currentTrackAtom) + + subtitle = subtitle || 'artist' + + return ( + + + {showArt ? : <>} + + + {song.title} + + {song[subtitle]} + + + + + + + + + + + + ) +} + +const styles = StyleSheet.create({ + container: { + marginBottom: 14, + minHeight: 50, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + item: { + flex: 1, + flexDirection: 'row', + justifyContent: 'flex-start', + }, + art: { + marginRight: 10, + }, + text: { + flex: 1, + }, + title: { + fontSize: 16, + fontFamily: font.semiBold, + }, + subtitle: { + fontSize: 14, + fontFamily: font.regular, + color: colors.text.secondary, + }, + controls: { + flexDirection: 'row', + alignItems: 'center', + marginLeft: 10, + }, + more: { + marginLeft: 8, + }, +}) + +export default React.memo(SongItem) diff --git a/app/models/music.ts b/app/models/music.ts index 4a23969..6a9fd7e 100644 --- a/app/models/music.ts +++ b/app/models/music.ts @@ -9,12 +9,14 @@ export interface ArtistInfo extends Artist { mediumImageUrl?: string largeImageUrl?: string - coverArtUris: string[] + albumCoverUris: string[] + + topSongs: Song[] } export interface ArtistArt { uri?: string - coverArtUris: string[] + albumCoverUris: string[] } export interface AlbumListItem { diff --git a/app/screens/AlbumView.tsx b/app/screens/AlbumView.tsx index f4cf511..a4811fb 100644 --- a/app/screens/AlbumView.tsx +++ b/app/screens/AlbumView.tsx @@ -1,78 +1,16 @@ +import Button from '@app/components/Button' +import CoverArt from '@app/components/CoverArt' +import GradientBackground from '@app/components/GradientBackground' +import ImageGradientScrollView from '@app/components/ImageGradientScrollView' +import SongItem from '@app/components/SongItem' +import { albumAtomFamily } from '@app/state/music' +import { useSetQueue } from '@app/state/trackplayer' +import colors from '@app/styles/colors' +import font from '@app/styles/font' import { useNavigation } from '@react-navigation/native' import { useAtomValue } from 'jotai/utils' import React, { useEffect } from 'react' -import { ActivityIndicator, GestureResponderEvent, StyleSheet, Text, View } from 'react-native' -import IconFA from 'react-native-vector-icons/FontAwesome' -import IconMat from 'react-native-vector-icons/MaterialIcons' -import { albumAtomFamily } from '@app/state/music' -import { currentTrackAtom, useSetQueue } from '@app/state/trackplayer' -import colors from '@app/styles/colors' -import font from '@app/styles/font' -import Button from '@app/components/Button' -import GradientBackground from '@app/components/GradientBackground' -import ImageGradientScrollView from '@app/components/ImageGradientScrollView' -import PressableOpacity from '@app/components/PressableOpacity' -import CoverArt from '@app/components/CoverArt' - -const SongItem: React.FC<{ - id: string - title: string - artist?: string - track?: number - onPress: (event: GestureResponderEvent) => void -}> = ({ id, title, artist, onPress }) => { - const currentTrack = useAtomValue(currentTrackAtom) - - return ( - - - - {title} - - {artist} - - - - - - - - - - - ) -} - -const songStyles = StyleSheet.create({ - container: { - marginTop: 20, - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - text: { - flex: 1, - alignItems: 'flex-start', - width: 100, - }, - title: { - fontSize: 16, - fontFamily: font.semiBold, - }, - subtitle: { - fontSize: 14, - fontFamily: font.regular, - color: colors.text.secondary, - }, - controls: { - flexDirection: 'row', - alignItems: 'center', - marginLeft: 10, - }, - more: { - marginLeft: 8, - }, -}) +import { ActivityIndicator, StyleSheet, Text, View } from 'react-native' const AlbumDetails: React.FC<{ id: string @@ -111,14 +49,7 @@ const AlbumDetails: React.FC<{ } }) .map(s => ( - setQueue(album.songs, album.name, s.id)} - /> + setQueue(album.songs, album.name, s.id)} /> ))} @@ -181,7 +112,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', }, songs: { - marginTop: 10, + marginTop: 26, marginBottom: 30, width: '100%', }, diff --git a/app/screens/ArtistView.tsx b/app/screens/ArtistView.tsx index 49839e9..cdd0b0b 100644 --- a/app/screens/ArtistView.tsx +++ b/app/screens/ArtistView.tsx @@ -1,6 +1,7 @@ import CoverArt from '@app/components/CoverArt' import GradientScrollView from '@app/components/GradientScrollView' import PressableOpacity from '@app/components/PressableOpacity' +import SongItem from '@app/components/SongItem' import { Album } from '@app/models/music' import { artistInfoAtomFamily } from '@app/state/music' import colors from '@app/styles/colors' @@ -22,7 +23,6 @@ const AlbumItem = React.memo<{ return ( navigation.navigate('AlbumView', { id: album.id, title: album.name })} - key={album.id} style={[styles.albumItem, { width }]}> {album.name} @@ -52,6 +52,10 @@ const ArtistDetails: React.FC<{ id: string }> = ({ id }) => { {artist.name} + Top Songs + {artist.topSongs.map(s => ( + + ))} Albums {artist.albums.map(a => ( @@ -112,7 +116,8 @@ const styles = StyleSheet.create({ fontFamily: font.bold, fontSize: 24, color: colors.text.primary, - marginTop: 14, + marginTop: 20, + marginBottom: 14, }, artistImage: { position: 'absolute', @@ -120,7 +125,6 @@ const styles = StyleSheet.create({ height: artistImageHeight, }, albums: { - marginTop: 14, width: '100%', flexDirection: 'row', flexWrap: 'wrap', diff --git a/app/state/music.ts b/app/state/music.ts index ba81b9a..1814996 100644 --- a/app/state/music.ts +++ b/app/state/music.ts @@ -131,7 +131,9 @@ export const artistInfoAtomFamily = atomFamily((id: string) => client.getArtist({ id }), client.getArtistInfo2({ id }), ]) - return mapArtistInfo(artistResponse.data, artistInfoResponse.data.artistInfo, client) + const topSongsResponse = await client.getTopSongs({ artist: artistResponse.data.artist.name, count: 50 }) + + return mapArtistInfo(artistResponse.data, artistInfoResponse.data.artistInfo, topSongsResponse.data.songs, client) }), ) @@ -142,7 +144,7 @@ export const artistArtAtomFamily = atomFamily((id: string) => return undefined } - const coverArtUris = artistInfo.albums + const albumCoverUris = artistInfo.albums .filter(a => a.coverArtThumbUri !== undefined) .sort((a, b) => { if (b.year && a.year) { @@ -154,7 +156,7 @@ export const artistArtAtomFamily = atomFamily((id: string) => .map(a => a.coverArtThumbUri) as string[] return { - coverArtUris, + albumCoverUris, uri: artistInfo.largeImageUrl, } }), @@ -171,12 +173,13 @@ function mapArtistID3toArtist(artist: ArtistID3Element): Artist { function mapArtistInfo( artistResponse: GetArtistResponse, info: ArtistInfo2Element, + topSongs: ChildElement[], client: SubsonicApiClient, ): ArtistInfo { const { artist, albums } = artistResponse const mappedAlbums = albums.map(a => mapAlbumID3toAlbum(a, client)) - const coverArtUris = mappedAlbums + const albumCoverUris = mappedAlbums .sort((a, b) => { if (a.year && b.year) { return b.year - a.year @@ -190,9 +193,10 @@ function mapArtistInfo( return { ...mapArtistID3toArtist(artist), albums: mappedAlbums, - coverArtUris, + albumCoverUris, mediumImageUrl: info.mediumImageUrl, largeImageUrl: info.largeImageUrl, + topSongs: topSongs.map(c => mapChildToSong(c, client)).slice(0, 5), } } diff --git a/app/subsonic/api.ts b/app/subsonic/api.ts index 60e70e8..48398a9 100644 --- a/app/subsonic/api.ts +++ b/app/subsonic/api.ts @@ -10,6 +10,7 @@ import { GetCoverArtParams, GetIndexesParams, GetMusicDirectoryParams, + GetTopSongsParams, StreamParams, } from '@app/subsonic/params' import { @@ -22,6 +23,7 @@ import { GetArtistsResponse, GetIndexesResponse, GetMusicDirectoryResponse, + GetTopSongsResponse, SubsonicResponse, } from '@app/subsonic/responses' import { Server } from '@app/models/settings' @@ -165,6 +167,11 @@ export class SubsonicApiClient { return new SubsonicResponse(xml, new GetArtistResponse(xml)) } + async getTopSongs(params: GetTopSongsParams): Promise> { + const xml = await this.apiGetXml('getTopSongs', params) + return new SubsonicResponse(xml, new GetTopSongsResponse(xml)) + } + // // Album/song lists // diff --git a/app/subsonic/params.ts b/app/subsonic/params.ts index 854eb13..c135835 100644 --- a/app/subsonic/params.ts +++ b/app/subsonic/params.ts @@ -27,6 +27,11 @@ export type GetArtistParams = { id: string } +export type GetTopSongsParams = { + artist: string + count?: number +} + // // Album/song lists // diff --git a/app/subsonic/responses.ts b/app/subsonic/responses.ts index 9d88e19..b063e5e 100644 --- a/app/subsonic/responses.ts +++ b/app/subsonic/responses.ts @@ -116,6 +116,17 @@ export class GetAlbumResponse { } } +export class GetTopSongsResponse { + songs: ChildElement[] = [] + + constructor(xml: Document) { + const childElements = xml.getElementsByTagName('song') + for (let i = 0; i < childElements.length; i++) { + this.songs.push(new ChildElement(childElements[i])) + } + } +} + // // Album/song lists //