diff --git a/app/components/ArtistArt.tsx b/app/components/ArtistArt.tsx index 2b1e8a3..ec039ba 100644 --- a/app/components/ArtistArt.tsx +++ b/app/components/ArtistArt.tsx @@ -164,7 +164,13 @@ const ArtistArt = React.memo(({ id, height, width }) => { return ( - + ) }) diff --git a/app/components/CoverArt.tsx b/app/components/CoverArt.tsx index 069ede4..ff2f8ce 100644 --- a/app/components/CoverArt.tsx +++ b/app/components/CoverArt.tsx @@ -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<{ { setLoading(false) setPlaceholderVisible(true) diff --git a/app/components/GradientBackground.tsx b/app/components/GradientBackground.tsx index c4ef9ce..4491a74 100644 --- a/app/components/GradientBackground.tsx +++ b/app/components/GradientBackground.tsx @@ -17,12 +17,14 @@ const GradientBackground: React.FC<{ + style={[ + style, + { + width: width || '100%', + height: height || layout.height, + position: position || 'absolute', + }, + ]}> {children} ) diff --git a/app/components/GradientScrollView.tsx b/app/components/GradientScrollView.tsx index bf6816d..8787770 100644 --- a/app/components/GradientScrollView.tsx +++ b/app/components/GradientScrollView.tsx @@ -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 = 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 = props => { {...props} style={[props.style, { backgroundColor: colors.gradient.low }]} contentContainerStyle={[props.contentContainerStyle, { minHeight }]}> - + {props.children} ) diff --git a/app/models/music.ts b/app/models/music.ts index 73b624e..4a23969 100644 --- a/app/models/music.ts +++ b/app/models/music.ts @@ -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 diff --git a/app/navigation/BottomTabNavigator.tsx b/app/navigation/BottomTabNavigator.tsx index b92f2f6..bfda6f2 100644 --- a/app/navigation/BottomTabNavigator.tsx +++ b/app/navigation/BottomTabNavigator.tsx @@ -19,13 +19,6 @@ type TabStackParamList = { ArtistView: { id: string; title: string } } -type TabMainScreenNavigationProp = NativeStackNavigationProp -type TabMainScreenRouteProp = RouteProp -type TabMainScreenProps = { - route: TabMainScreenRouteProp - navigation: TabMainScreenNavigationProp -} - type AlbumScreenNavigationProp = NativeStackNavigationProp type AlbumScreenRouteProp = RouteProp type AlbumScreenProps = { diff --git a/app/screens/ArtistView.tsx b/app/screens/ArtistView.tsx index 1e8a66f..49839e9 100644 --- a/app/screens/ArtistView.tsx +++ b/app/screens/ArtistView.tsx @@ -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 ( + navigation.navigate('AlbumView', { id: album.id, title: album.name })} + key={album.id} + style={[styles.albumItem, { width }]}> + + {album.name} + {album.year ? album.year : ''} + + ) +}) 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 ( - - {artist.name} - + + + + {artist.name} + + + Albums + + {artist.albums.map(a => ( + + ))} + + ) } @@ -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', }, }) diff --git a/app/screens/Home.tsx b/app/screens/Home.tsx index fdf950c..e27da57 100644 --- a/app/screens/Home.tsx +++ b/app/screens/Home.tsx @@ -38,7 +38,7 @@ const Category = React.memo<{ }>(({ name, data }) => { return ( - {name} + {name} { 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)), } }