mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 17:39:27 +01:00
redid cover art (again...) and impl a ListItem
This commit is contained in:
parent
6dd17f2797
commit
fbf6060db4
@ -16,7 +16,7 @@
|
|||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
|
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
|
||||||
android:launchMode="singleTask"
|
android:launchMode="singleTask"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustPan">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
|||||||
@ -1,207 +0,0 @@
|
|||||||
import CoverArt from '@app/components/CoverArt'
|
|
||||||
import { artistArtAtomFamily } from '@app/state/music'
|
|
||||||
import colors from '@app/styles/colors'
|
|
||||||
import { useLayout } from '@react-native-community/hooks'
|
|
||||||
import { useAtomValue } from 'jotai/utils'
|
|
||||||
import React from 'react'
|
|
||||||
import { ActivityIndicator, StyleSheet, View } from 'react-native'
|
|
||||||
import FastImage from 'react-native-fast-image'
|
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
|
||||||
import IconFA5 from 'react-native-vector-icons/FontAwesome5'
|
|
||||||
|
|
||||||
interface ArtistArtSizeProps {
|
|
||||||
height: number
|
|
||||||
width: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArtistArtXUpProps extends ArtistArtSizeProps {
|
|
||||||
albumCoverUris: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ArtistArtProps extends ArtistArtSizeProps {
|
|
||||||
id: string
|
|
||||||
round?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const PlaceholderContainer: React.FC<ArtistArtSizeProps> = ({ height, width, children }) => {
|
|
||||||
const layout = useLayout()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LinearGradient
|
|
||||||
onLayout={layout.onLayout}
|
|
||||||
colors={[colors.accent, colors.accentLow]}
|
|
||||||
style={[styles.placeholderContainer, { height, width }]}>
|
|
||||||
<IconFA5 name="microphone" color="black" size={layout.width / 1.8} style={styles.placeholderIcon} />
|
|
||||||
{children}
|
|
||||||
</LinearGradient>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const FourUp = React.memo<ArtistArtXUpProps>(({ height, width, albumCoverUris }) => {
|
|
||||||
const halfHeight = height / 2
|
|
||||||
const halfWidth = width / 2
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PlaceholderContainer height={height} width={width}>
|
|
||||||
<View style={[styles.artRow, { width, height: halfHeight }]}>
|
|
||||||
<FastImage
|
|
||||||
source={{ uri: albumCoverUris[0] }}
|
|
||||||
style={{ height: halfHeight, width: halfWidth }}
|
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
|
||||||
/>
|
|
||||||
<FastImage
|
|
||||||
source={{ uri: albumCoverUris[1] }}
|
|
||||||
style={{ height: halfHeight, width: halfWidth }}
|
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={[styles.artRow, { width, height: halfHeight }]}>
|
|
||||||
<FastImage
|
|
||||||
source={{ uri: albumCoverUris[2] }}
|
|
||||||
style={{ height: halfHeight, width: halfWidth }}
|
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
|
||||||
/>
|
|
||||||
<FastImage
|
|
||||||
source={{ uri: albumCoverUris[3] }}
|
|
||||||
style={{ height: halfHeight, width: halfWidth }}
|
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</PlaceholderContainer>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const ThreeUp = React.memo<ArtistArtXUpProps>(({ height, width, albumCoverUris }) => {
|
|
||||||
const halfHeight = height / 2
|
|
||||||
const halfWidth = width / 2
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PlaceholderContainer height={height} width={width}>
|
|
||||||
<View style={[styles.artRow, { width, height: halfHeight }]}>
|
|
||||||
<FastImage
|
|
||||||
source={{ uri: albumCoverUris[0] }}
|
|
||||||
style={{ height: halfHeight, width }}
|
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={[styles.artRow, { width, height: halfHeight }]}>
|
|
||||||
<FastImage
|
|
||||||
source={{ uri: albumCoverUris[1] }}
|
|
||||||
style={{ height: halfHeight, width: halfWidth }}
|
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
|
||||||
/>
|
|
||||||
<FastImage
|
|
||||||
source={{ uri: albumCoverUris[2] }}
|
|
||||||
style={{ height: halfHeight, width: halfWidth }}
|
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</PlaceholderContainer>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const TwoUp = React.memo<ArtistArtXUpProps>(({ height, width, albumCoverUris }) => {
|
|
||||||
const halfHeight = height / 2
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PlaceholderContainer height={height} width={width}>
|
|
||||||
<View style={[styles.artRow, { width, height: halfHeight }]}>
|
|
||||||
<FastImage
|
|
||||||
source={{ uri: albumCoverUris[0] }}
|
|
||||||
style={{ height: halfHeight, width }}
|
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
<View style={[styles.artRow, { width, height: halfHeight }]}>
|
|
||||||
<FastImage
|
|
||||||
source={{ uri: albumCoverUris[1] }}
|
|
||||||
style={{ height: halfHeight, width }}
|
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</PlaceholderContainer>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const OneUp = React.memo<ArtistArtXUpProps>(({ height, width, albumCoverUris }) => (
|
|
||||||
<PlaceholderContainer height={height} width={width}>
|
|
||||||
<FastImage source={{ uri: albumCoverUris[0] }} style={{ height, width }} resizeMode={FastImage.resizeMode.cover} />
|
|
||||||
</PlaceholderContainer>
|
|
||||||
))
|
|
||||||
|
|
||||||
const NoneUp = React.memo<ArtistArtSizeProps>(({ height, width }) => (
|
|
||||||
<PlaceholderContainer height={height} width={width} />
|
|
||||||
))
|
|
||||||
|
|
||||||
const ArtistArt = React.memo<ArtistArtProps>(({ id, height, width, round }) => {
|
|
||||||
const artistArt = useAtomValue(artistArtAtomFamily(id))
|
|
||||||
|
|
||||||
round = round === undefined ? true : round
|
|
||||||
|
|
||||||
const Placeholder = () => {
|
|
||||||
if (!artistArt) {
|
|
||||||
return <NoneUp height={height} width={width} />
|
|
||||||
}
|
|
||||||
const { albumCoverUris } = artistArt
|
|
||||||
|
|
||||||
if (albumCoverUris.length >= 4) {
|
|
||||||
return <FourUp height={height} width={width} albumCoverUris={albumCoverUris} />
|
|
||||||
}
|
|
||||||
if (albumCoverUris.length === 3) {
|
|
||||||
return <ThreeUp height={height} width={width} albumCoverUris={albumCoverUris} />
|
|
||||||
}
|
|
||||||
if (albumCoverUris.length === 2) {
|
|
||||||
return <TwoUp height={height} width={width} albumCoverUris={albumCoverUris} />
|
|
||||||
}
|
|
||||||
if (albumCoverUris.length === 1) {
|
|
||||||
return <OneUp height={height} width={width} albumCoverUris={albumCoverUris} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return <NoneUp height={height} width={width} />
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={[styles.container, round ? { borderRadius: height / 2 } : {}]}>
|
|
||||||
<CoverArt
|
|
||||||
FallbackComponent={Placeholder}
|
|
||||||
style={{ height, width }}
|
|
||||||
coverArtUri={artistArt?.uri}
|
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const ArtistArtFallback = React.memo<ArtistArtProps>(({ height, width }) => (
|
|
||||||
<View style={[styles.fallback, { height, width }]}>
|
|
||||||
<ActivityIndicator size="large" color={colors.accent} />
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
|
|
||||||
const ArtistArtLoader: React.FC<ArtistArtProps> = props => (
|
|
||||||
<React.Suspense fallback={<ArtistArtFallback {...props} />}>
|
|
||||||
<ArtistArt {...props} />
|
|
||||||
</React.Suspense>
|
|
||||||
)
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
placeholderContainer: {
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
placeholderIcon: {
|
|
||||||
position: 'absolute',
|
|
||||||
},
|
|
||||||
artRow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
fallback: {
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default React.memo(ArtistArtLoader)
|
|
||||||
@ -1,69 +1,124 @@
|
|||||||
|
import { artistInfoAtomFamily, useCoverArtUri } from '@app/state/music'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import React, { useState } from 'react'
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import { ActivityIndicator, StyleSheet, View } from 'react-native'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { ActivityIndicator, StyleSheet, View, ViewStyle } from 'react-native'
|
||||||
import FastImage, { ImageStyle } from 'react-native-fast-image'
|
import FastImage, { ImageStyle } from 'react-native-fast-image'
|
||||||
import LinearGradient from 'react-native-linear-gradient'
|
|
||||||
|
|
||||||
type CoverImageProps = {
|
type BaseProps = {
|
||||||
uri?: string
|
imageSize?: 'thumbnail' | 'original'
|
||||||
style?: ImageStyle
|
style?: ViewStyle
|
||||||
|
imageStyle?: ImageStyle
|
||||||
resizeMode?: keyof typeof FastImage.resizeMode
|
resizeMode?: keyof typeof FastImage.resizeMode
|
||||||
onProgress?: () => void
|
round?: boolean
|
||||||
onLoadEnd?: () => void
|
|
||||||
onError?: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CoverImage = React.memo<CoverImageProps>(({ uri, style, resizeMode, onProgress, onLoadEnd, onError }) => (
|
type BaseImageProps = BaseProps & {
|
||||||
|
enableLoading: () => void
|
||||||
|
disableLoading: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArtistIdProp = {
|
||||||
|
artistId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type CoverArtProp = {
|
||||||
|
coverArt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ArtistIdImageProps = BaseImageProps & ArtistIdProp
|
||||||
|
type CoverArtImageProps = BaseImageProps & CoverArtProp
|
||||||
|
|
||||||
|
type CoverArtProps = BaseProps & CoverArtProp & Partial<ArtistIdProp>
|
||||||
|
|
||||||
|
const ArtistIdImageLoaded = React.memo<ArtistIdImageProps>(
|
||||||
|
({ artistId, imageSize, style, imageStyle, resizeMode, enableLoading, disableLoading }) => {
|
||||||
|
const artistInfo = useAtomValue(artistInfoAtomFamily(artistId))
|
||||||
|
|
||||||
|
const uri = imageSize === 'thumbnail' ? artistInfo?.smallImageUrl : artistInfo?.largeImageUrl
|
||||||
|
|
||||||
|
return (
|
||||||
<FastImage
|
<FastImage
|
||||||
source={{ uri }}
|
source={{ uri }}
|
||||||
style={style}
|
style={[{ height: style?.height, width: style?.width }, imageStyle]}
|
||||||
resizeMode={resizeMode || FastImage.resizeMode.contain}
|
resizeMode={resizeMode || FastImage.resizeMode.contain}
|
||||||
onProgress={onProgress}
|
onProgress={enableLoading}
|
||||||
onLoadEnd={onLoadEnd}
|
onLoadEnd={disableLoading}
|
||||||
onError={onError}
|
|
||||||
/>
|
/>
|
||||||
))
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
const Fallback = React.memo<{}>(({}) => {
|
const ArtistIdImageFallback: React.FC<{
|
||||||
return <LinearGradient colors={[colors.accent, colors.accentLow]} style={styles.fallback} />
|
enableLoading: () => void
|
||||||
|
}> = ({ enableLoading }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
enableLoading()
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [])
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
const ArtistIdImage = React.memo<ArtistIdImageProps>(props => {
|
||||||
|
return (
|
||||||
|
<React.Suspense fallback={<ArtistIdImageFallback enableLoading={props.enableLoading} />}>
|
||||||
|
<ArtistIdImageLoaded {...props} />
|
||||||
|
</React.Suspense>
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const CoverArt: React.FC<{
|
const CoverArtImage = React.memo<CoverArtImageProps>(
|
||||||
FallbackComponent?: () => JSX.Element
|
({ coverArt, imageSize, style, imageStyle, resizeMode, enableLoading, disableLoading }) => {
|
||||||
placeholderIcon?: string
|
const coverArtUri = useCoverArtUri()
|
||||||
height?: string | number
|
|
||||||
width?: string | number
|
return (
|
||||||
coverArtUri?: string
|
<FastImage
|
||||||
resizeMode?: keyof typeof FastImage.resizeMode
|
source={{ uri: coverArtUri(coverArt, imageSize) }}
|
||||||
style?: ImageStyle
|
style={[{ height: style?.height, width: style?.width }, imageStyle]}
|
||||||
}> = ({ FallbackComponent, coverArtUri, resizeMode, style }) => {
|
resizeMode={resizeMode || FastImage.resizeMode.contain}
|
||||||
|
onProgress={enableLoading}
|
||||||
|
onLoadEnd={disableLoading}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const CoverArt: React.FC<CoverArtProps> = ({ coverArt, artistId, resizeMode, imageSize, style, imageStyle, round }) => {
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [fallbackVisible, setFallbackVisible] = useState(false)
|
|
||||||
|
|
||||||
const enableLoading = React.useCallback(() => setLoading(true), [])
|
const enableLoading = React.useCallback(() => setLoading(true), [])
|
||||||
const disableLoading = React.useCallback(() => setLoading(false), [])
|
const disableLoading = React.useCallback(() => setLoading(false), [])
|
||||||
const enableFallback = React.useCallback(() => setFallbackVisible(true), [])
|
|
||||||
|
imageSize = imageSize === undefined ? 'thumbnail' : 'original'
|
||||||
|
round = round === undefined ? artistId !== undefined : round
|
||||||
|
|
||||||
|
const viewStyles = [style]
|
||||||
|
if (round) {
|
||||||
|
viewStyles.push(styles.round)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={style}>
|
<View style={viewStyles}>
|
||||||
<CoverImage
|
{artistId ? (
|
||||||
uri={coverArtUri}
|
<ArtistIdImage
|
||||||
|
artistId={artistId}
|
||||||
|
imageSize={imageSize}
|
||||||
style={style}
|
style={style}
|
||||||
|
imageStyle={imageStyle}
|
||||||
resizeMode={resizeMode}
|
resizeMode={resizeMode}
|
||||||
onProgress={enableLoading}
|
enableLoading={enableLoading}
|
||||||
onLoadEnd={disableLoading}
|
disableLoading={disableLoading}
|
||||||
onError={enableFallback}
|
|
||||||
/>
|
/>
|
||||||
{fallbackVisible ? (
|
|
||||||
FallbackComponent ? (
|
|
||||||
<View style={styles.fallback}>
|
|
||||||
<FallbackComponent />
|
|
||||||
</View>
|
|
||||||
) : (
|
) : (
|
||||||
<Fallback />
|
<CoverArtImage
|
||||||
)
|
coverArt={coverArt}
|
||||||
) : (
|
imageSize={imageSize}
|
||||||
<></>
|
style={style}
|
||||||
|
imageStyle={imageStyle}
|
||||||
|
resizeMode={resizeMode}
|
||||||
|
enableLoading={enableLoading}
|
||||||
|
disableLoading={disableLoading}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<ActivityIndicator animating={loading} size="large" color={colors.accent} style={styles.indicator} />
|
<ActivityIndicator animating={loading} size="large" color={colors.accent} style={styles.indicator} />
|
||||||
</View>
|
</View>
|
||||||
@ -71,16 +126,9 @@ const CoverArt: React.FC<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
image: {
|
round: {
|
||||||
height: '100%',
|
overflow: 'hidden',
|
||||||
width: '100%',
|
borderRadius: 1000,
|
||||||
},
|
|
||||||
fallback: {
|
|
||||||
height: '100%',
|
|
||||||
width: '100%',
|
|
||||||
position: 'absolute',
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
},
|
||||||
indicator: {
|
indicator: {
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { FlatList, FlatListProps, useWindowDimensions } from 'react-native'
|
import { FlatList, FlatListProps, StyleSheet, useWindowDimensions } from 'react-native'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import GradientBackground from '@app/components/GradientBackground'
|
import GradientBackground from '@app/components/GradientBackground'
|
||||||
|
|
||||||
function GradientFlatList<ItemT>(props: FlatListProps<ItemT>) {
|
function GradientFlatList<ItemT>(props: FlatListProps<ItemT>) {
|
||||||
const layout = useWindowDimensions()
|
const layout = useWindowDimensions()
|
||||||
|
|
||||||
|
const contentContainerStyle = StyleSheet.flatten(props.contentContainerStyle)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FlatList
|
<FlatList
|
||||||
{...props}
|
{...props}
|
||||||
@ -16,6 +18,8 @@ function GradientFlatList<ItemT>(props: FlatListProps<ItemT>) {
|
|||||||
ListHeaderComponent={() => <GradientBackground position="relative" />}
|
ListHeaderComponent={() => <GradientBackground position="relative" />}
|
||||||
ListHeaderComponentStyle={{
|
ListHeaderComponentStyle={{
|
||||||
marginBottom: -layout.height,
|
marginBottom: -layout.height,
|
||||||
|
marginHorizontal: -(contentContainerStyle.paddingHorizontal || 0),
|
||||||
|
top: -(contentContainerStyle.paddingTop || 0),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|||||||
22
app/components/Header.tsx
Normal file
22
app/components/Header.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import colors from '@app/styles/colors'
|
||||||
|
import font from '@app/styles/font'
|
||||||
|
import React from 'react'
|
||||||
|
import { StyleSheet, Text, TextStyle } from 'react-native'
|
||||||
|
|
||||||
|
const Header: React.FC<{
|
||||||
|
style?: TextStyle
|
||||||
|
}> = ({ children, style }) => {
|
||||||
|
return <Text style={[styles.text, style]}>{children}</Text>
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
text: {
|
||||||
|
fontFamily: font.bold,
|
||||||
|
fontSize: 24,
|
||||||
|
color: colors.text.primary,
|
||||||
|
marginTop: 18,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Header
|
||||||
171
app/components/ListItem.tsx
Normal file
171
app/components/ListItem.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
import { AlbumListItem, Artist, PlaylistListItem, 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, { useState } from 'react'
|
||||||
|
import { GestureResponderEvent, StyleSheet, Text, View } from 'react-native'
|
||||||
|
import IconFA from 'react-native-vector-icons/FontAwesome'
|
||||||
|
import IconFA5 from 'react-native-vector-icons/FontAwesome5'
|
||||||
|
import IconMat from 'react-native-vector-icons/MaterialIcons'
|
||||||
|
import CoverArt from './CoverArt'
|
||||||
|
import PressableOpacity from './PressableOpacity'
|
||||||
|
|
||||||
|
const TitleTextSong = React.memo<{
|
||||||
|
id: string
|
||||||
|
title?: string
|
||||||
|
}>(({ id, title }) => {
|
||||||
|
const currentTrack = useAtomValue(currentTrackAtom)
|
||||||
|
const playing = currentTrack?.id === id
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.textLine}>
|
||||||
|
{playing ? <IconFA5 name="play" size={10} color={colors.accent} style={styles.playingIcon} /> : <></>}
|
||||||
|
<Text style={[styles.title, { color: playing ? colors.accent : colors.text.primary }]}>{title}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const TitleText = React.memo<{
|
||||||
|
title?: string
|
||||||
|
}>(({ title }) => {
|
||||||
|
return (
|
||||||
|
<View style={styles.textLine}>
|
||||||
|
<Text style={styles.title}>{title}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const ListItem: React.FC<{
|
||||||
|
item: Song | AlbumListItem | Artist | PlaylistListItem
|
||||||
|
onPress?: (event: GestureResponderEvent) => void
|
||||||
|
showArt?: boolean
|
||||||
|
showStar?: boolean
|
||||||
|
listStyle?: 'big' | 'small'
|
||||||
|
subtitle?: string
|
||||||
|
}> = ({ item, onPress, showArt, showStar, subtitle, listStyle }) => {
|
||||||
|
const [starred, setStarred] = useState(false)
|
||||||
|
|
||||||
|
showStar = showStar === undefined ? true : showStar
|
||||||
|
listStyle = listStyle || 'small'
|
||||||
|
|
||||||
|
const artSource = item.itemType === 'artist' ? { artistId: item.id } : { coverArt: item.coverArt }
|
||||||
|
const sizeStyle = listStyle === 'big' ? bigStyles : smallStyles
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, sizeStyle.container]}>
|
||||||
|
<PressableOpacity onPress={onPress} style={styles.item}>
|
||||||
|
{showArt ? <CoverArt {...artSource} style={{ ...styles.art, ...sizeStyle.art }} resizeMode="cover" /> : <></>}
|
||||||
|
<View style={styles.text}>
|
||||||
|
{item.itemType === 'song' ? (
|
||||||
|
<TitleTextSong id={item.id} title={item.title} />
|
||||||
|
) : (
|
||||||
|
<TitleText title={item.name} />
|
||||||
|
)}
|
||||||
|
{subtitle ? (
|
||||||
|
<View style={styles.textLine}>
|
||||||
|
{starred ? (
|
||||||
|
<IconMat
|
||||||
|
name="file-download-done"
|
||||||
|
size={17}
|
||||||
|
color={colors.text.secondary}
|
||||||
|
style={styles.downloadedIcon}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
<Text style={styles.subtitle}>{subtitle}</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</PressableOpacity>
|
||||||
|
<View style={styles.controls}>
|
||||||
|
{showStar ? (
|
||||||
|
<PressableOpacity onPress={() => setStarred(!starred)} style={styles.controlItem}>
|
||||||
|
{starred ? (
|
||||||
|
<IconFA name="star" size={26} color={colors.accent} />
|
||||||
|
) : (
|
||||||
|
<IconFA name="star-o" size={26} color={colors.text.secondary} />
|
||||||
|
)}
|
||||||
|
</PressableOpacity>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
marginBottom: 14,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
},
|
||||||
|
art: {
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
textLine: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontFamily: font.semiBold,
|
||||||
|
color: colors.text.primary,
|
||||||
|
},
|
||||||
|
playingIcon: {
|
||||||
|
marginRight: 5,
|
||||||
|
marginLeft: 1,
|
||||||
|
},
|
||||||
|
downloadedIcon: {
|
||||||
|
marginRight: 2,
|
||||||
|
marginLeft: -3,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: font.regular,
|
||||||
|
color: colors.text.secondary,
|
||||||
|
},
|
||||||
|
controls: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
controlItem: {
|
||||||
|
marginLeft: 16,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const smallStyles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
minHeight: 50,
|
||||||
|
},
|
||||||
|
art: {
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const bigStyles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
minHeight: 70,
|
||||||
|
},
|
||||||
|
art: {
|
||||||
|
height: 70,
|
||||||
|
width: 70,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default React.memo(ListItem)
|
||||||
@ -70,7 +70,7 @@ const NowPlayingBar = () => {
|
|||||||
<View style={styles.subContainer}>
|
<View style={styles.subContainer}>
|
||||||
<CoverArt
|
<CoverArt
|
||||||
style={{ height: styles.subContainer.height, width: styles.subContainer.height }}
|
style={{ height: styles.subContainer.height, width: styles.subContainer.height }}
|
||||||
coverArtUri={track?.artworkThumb}
|
coverArt={track?.coverArt || '-1'}
|
||||||
/>
|
/>
|
||||||
<View style={styles.detailsContainer}>
|
<View style={styles.detailsContainer}>
|
||||||
<Text numberOfLines={1} style={styles.detailsTitle}>
|
<Text numberOfLines={1} style={styles.detailsTitle}>
|
||||||
|
|||||||
@ -1,116 +0,0 @@
|
|||||||
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, { useState } from 'react'
|
|
||||||
import { GestureResponderEvent, StyleSheet, Text, View } from 'react-native'
|
|
||||||
import IconFA from 'react-native-vector-icons/FontAwesome'
|
|
||||||
import IconFA5 from 'react-native-vector-icons/FontAwesome5'
|
|
||||||
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)
|
|
||||||
const [starred, setStarred] = useState(false)
|
|
||||||
|
|
||||||
subtitle = subtitle || 'artist'
|
|
||||||
const playing = currentTrack?.id === song.id
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={styles.container}>
|
|
||||||
<PressableOpacity onPress={onPress} style={styles.item}>
|
|
||||||
{showArt ? <CoverArt coverArtUri={song.coverArtThumbUri} style={styles.art} /> : <></>}
|
|
||||||
<View style={styles.text}>
|
|
||||||
<View style={styles.textLine}>
|
|
||||||
{playing ? <IconFA5 name="play" size={10} color={colors.accent} style={styles.playingIcon} /> : <></>}
|
|
||||||
<Text style={[styles.title, { color: playing ? colors.accent : colors.text.primary }]}>{song.title}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.textLine}>
|
|
||||||
{starred ? (
|
|
||||||
<IconMat
|
|
||||||
name="file-download-done"
|
|
||||||
size={17}
|
|
||||||
color={colors.text.secondary}
|
|
||||||
style={styles.downloadedIcon}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
<Text style={styles.subtitle}>{song[subtitle]}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</PressableOpacity>
|
|
||||||
<View style={styles.controls}>
|
|
||||||
<PressableOpacity onPress={() => setStarred(!starred)}>
|
|
||||||
{starred ? (
|
|
||||||
<IconFA name="star" size={26} color={colors.accent} />
|
|
||||||
) : (
|
|
||||||
<IconFA name="star-o" size={26} color={colors.text.secondary} />
|
|
||||||
)}
|
|
||||||
</PressableOpacity>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
height: 50,
|
|
||||||
width: 50,
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
textLine: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: 16,
|
|
||||||
fontFamily: font.semiBold,
|
|
||||||
},
|
|
||||||
playingIcon: {
|
|
||||||
marginRight: 5,
|
|
||||||
marginLeft: 1,
|
|
||||||
},
|
|
||||||
downloadedIcon: {
|
|
||||||
marginRight: 2,
|
|
||||||
marginLeft: -3,
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontFamily: font.regular,
|
|
||||||
color: colors.text.secondary,
|
|
||||||
},
|
|
||||||
controls: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
marginLeft: 16,
|
|
||||||
},
|
|
||||||
more: {
|
|
||||||
marginLeft: 8,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export default React.memo(SongItem)
|
|
||||||
@ -1,34 +1,32 @@
|
|||||||
export interface Artist {
|
export interface Artist {
|
||||||
|
itemType: 'artist'
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
starred?: Date
|
starred?: Date
|
||||||
|
coverArt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArtistInfo extends Artist {
|
export interface ArtistInfo extends Artist {
|
||||||
albums: Album[]
|
albums: Album[]
|
||||||
|
|
||||||
|
smallImageUrl?: string
|
||||||
mediumImageUrl?: string
|
mediumImageUrl?: string
|
||||||
largeImageUrl?: string
|
largeImageUrl?: string
|
||||||
albumCoverUris: string[]
|
|
||||||
|
|
||||||
topSongs: Song[]
|
topSongs: Song[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ArtistArt {
|
|
||||||
uri?: string
|
|
||||||
albumCoverUris: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AlbumListItem {
|
export interface AlbumListItem {
|
||||||
|
itemType: 'album'
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
artist?: string
|
artist?: string
|
||||||
starred?: Date
|
starred?: Date
|
||||||
coverArtThumbUri?: string
|
coverArt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Album extends AlbumListItem {
|
export interface Album extends AlbumListItem {
|
||||||
coverArtUri?: string
|
coverArt?: string
|
||||||
year?: number
|
year?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,19 +34,27 @@ export interface AlbumWithSongs extends Album {
|
|||||||
songs: Song[]
|
songs: Song[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SearchResults {
|
||||||
|
artists: Artist[]
|
||||||
|
albums: AlbumListItem[]
|
||||||
|
songs: Song[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface PlaylistListItem {
|
export interface PlaylistListItem {
|
||||||
|
itemType: 'playlist'
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
comment?: string
|
comment?: string
|
||||||
coverArtThumbUri?: string
|
coverArt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlaylistWithSongs extends PlaylistListItem {
|
export interface PlaylistWithSongs extends PlaylistListItem {
|
||||||
songs: Song[]
|
songs: Song[]
|
||||||
coverArtUri?: string
|
coverArt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Song {
|
export interface Song {
|
||||||
|
itemType: 'song'
|
||||||
id: string
|
id: string
|
||||||
album?: string
|
album?: string
|
||||||
artist?: string
|
artist?: string
|
||||||
@ -58,8 +64,7 @@ export interface Song {
|
|||||||
starred?: Date
|
starred?: Date
|
||||||
|
|
||||||
streamUri: string
|
streamUri: string
|
||||||
coverArtUri?: string
|
coverArt?: string
|
||||||
coverArtThumbUri?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DownloadedSong = {
|
export type DownloadedSong = {
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import BottomTabBar from '@app/navigation/BottomTabBar'
|
import BottomTabBar from '@app/navigation/BottomTabBar'
|
||||||
import LibraryTopTabNavigator from '@app/navigation/LibraryTopTabNavigator'
|
import LibraryTopTabNavigator from '@app/navigation/LibraryTopTabNavigator'
|
||||||
import AlbumView from '@app/screens/AlbumView'
|
import AlbumView from '@app/screens/AlbumView'
|
||||||
import ArtistsList from '@app/screens/ArtistsList'
|
|
||||||
import ArtistView from '@app/screens/ArtistView'
|
import ArtistView from '@app/screens/ArtistView'
|
||||||
import Home from '@app/screens/Home'
|
import Home from '@app/screens/Home'
|
||||||
import PlaylistView from '@app/screens/PlaylistView'
|
import PlaylistView from '@app/screens/PlaylistView'
|
||||||
|
import Search from '@app/screens/Search'
|
||||||
import SettingsView from '@app/screens/Settings'
|
import SettingsView from '@app/screens/Settings'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import font from '@app/styles/font'
|
import font from '@app/styles/font'
|
||||||
@ -98,7 +98,7 @@ function createTabStackNavigator(Component: React.ComponentType<any>) {
|
|||||||
|
|
||||||
const HomeTab = createTabStackNavigator(Home)
|
const HomeTab = createTabStackNavigator(Home)
|
||||||
const LibraryTab = createTabStackNavigator(LibraryTopTabNavigator)
|
const LibraryTab = createTabStackNavigator(LibraryTopTabNavigator)
|
||||||
const SearchTab = createTabStackNavigator(ArtistsList)
|
const SearchTab = createTabStackNavigator(Search)
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator()
|
const Tab = createBottomTabNavigator()
|
||||||
|
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import GradientBackground from '@app/components/GradientBackground'
|
|||||||
import ImageGradientScrollView from '@app/components/ImageGradientScrollView'
|
import ImageGradientScrollView from '@app/components/ImageGradientScrollView'
|
||||||
import ListPlayerControls from '@app/components/ListPlayerControls'
|
import ListPlayerControls from '@app/components/ListPlayerControls'
|
||||||
import NothingHere from '@app/components/NothingHere'
|
import NothingHere from '@app/components/NothingHere'
|
||||||
import SongItem from '@app/components/SongItem'
|
import ListItem from '@app/components/ListItem'
|
||||||
import { albumAtomFamily } from '@app/state/music'
|
import { albumAtomFamily, useCoverArtUri } from '@app/state/music'
|
||||||
import { useSetQueue } from '@app/state/trackplayer'
|
import { useSetQueue } from '@app/state/trackplayer'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import font from '@app/styles/font'
|
import font from '@app/styles/font'
|
||||||
@ -17,6 +17,7 @@ const AlbumDetails: React.FC<{
|
|||||||
id: string
|
id: string
|
||||||
}> = ({ id }) => {
|
}> = ({ id }) => {
|
||||||
const album = useAtomValue(albumAtomFamily(id))
|
const album = useAtomValue(albumAtomFamily(id))
|
||||||
|
const coverArtUri = useCoverArtUri()
|
||||||
const setQueue = useSetQueue()
|
const setQueue = useSetQueue()
|
||||||
|
|
||||||
if (!album) {
|
if (!album) {
|
||||||
@ -36,7 +37,7 @@ const AlbumDetails: React.FC<{
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map((s, i) => (
|
.map((s, i) => (
|
||||||
<SongItem key={i} song={s} onPress={() => setQueue(album.songs, album.name, i)} />
|
<ListItem key={i} item={s} subtitle={s.artist} onPress={() => setQueue(album.songs, album.name, i)} />
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
@ -44,11 +45,11 @@ const AlbumDetails: React.FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageGradientScrollView
|
<ImageGradientScrollView
|
||||||
imageUri={album.coverArtThumbUri}
|
imageUri={coverArtUri(album.coverArt)}
|
||||||
imageKey={`${album.name}${album.artist}`}
|
imageKey={`${album.name}${album.artist}`}
|
||||||
style={styles.container}>
|
style={styles.container}>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<CoverArt coverArtUri={album.coverArtUri} style={styles.cover} />
|
<CoverArt coverArt={album.coverArt} style={styles.cover} imageSize="original" />
|
||||||
<Text style={styles.title}>{album.name}</Text>
|
<Text style={styles.title}>{album.name}</Text>
|
||||||
<Text style={styles.subtitle}>
|
<Text style={styles.subtitle}>
|
||||||
{album.artist}
|
{album.artist}
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import ArtistArt from '@app/components/ArtistArt'
|
|
||||||
import CoverArt from '@app/components/CoverArt'
|
import CoverArt from '@app/components/CoverArt'
|
||||||
import GradientScrollView from '@app/components/GradientScrollView'
|
import GradientScrollView from '@app/components/GradientScrollView'
|
||||||
|
import Header from '@app/components/Header'
|
||||||
import PressableOpacity from '@app/components/PressableOpacity'
|
import PressableOpacity from '@app/components/PressableOpacity'
|
||||||
import SongItem from '@app/components/SongItem'
|
import ListItem from '@app/components/ListItem'
|
||||||
import { Album } from '@app/models/music'
|
import { Album } from '@app/models/music'
|
||||||
import { artistInfoAtomFamily } from '@app/state/music'
|
import { artistInfoAtomFamily } from '@app/state/music'
|
||||||
import { useSetQueue } from '@app/state/trackplayer'
|
import { useSetQueue } from '@app/state/trackplayer'
|
||||||
@ -26,7 +26,7 @@ const AlbumItem = React.memo<{
|
|||||||
<PressableOpacity
|
<PressableOpacity
|
||||||
onPress={() => navigation.navigate('AlbumView', { id: album.id, title: album.name })}
|
onPress={() => navigation.navigate('AlbumView', { id: album.id, title: album.name })}
|
||||||
style={[styles.albumItem, { width }]}>
|
style={[styles.albumItem, { width }]}>
|
||||||
<CoverArt coverArtUri={album.coverArtThumbUri} style={{ height, width }} />
|
<CoverArt coverArt={album.coverArt} style={{ height, width }} />
|
||||||
<Text style={styles.albumTitle}>{album.name}</Text>
|
<Text style={styles.albumTitle}>{album.name}</Text>
|
||||||
<Text style={styles.albumYear}> {album.year ? album.year : ''}</Text>
|
<Text style={styles.albumYear}> {album.year ? album.year : ''}</Text>
|
||||||
</PressableOpacity>
|
</PressableOpacity>
|
||||||
@ -47,25 +47,19 @@ const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
|
|||||||
|
|
||||||
const TopSongs = () => (
|
const TopSongs = () => (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.header}>Top Songs</Text>
|
<Header>Top Songs</Header>
|
||||||
{artist.topSongs.map((s, i) => (
|
{artist.topSongs.map((s, i) => (
|
||||||
<SongItem
|
<ListItem
|
||||||
key={i}
|
key={i}
|
||||||
song={s}
|
item={s}
|
||||||
showArt={true}
|
showArt={true}
|
||||||
subtitle="album"
|
subtitle={s.album}
|
||||||
onPress={() => setQueue(artist.topSongs, `Top Songs: ${artist.name}`, i)}
|
onPress={() => setQueue(artist.topSongs, `Top Songs: ${artist.name}`, i)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
const ArtistCoverFallback = () => (
|
|
||||||
<View style={styles.artistCover}>
|
|
||||||
<ArtistArt id={artist.id} round={false} height={artistCoverHeight} width={coverLayout.width} />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GradientScrollView
|
<GradientScrollView
|
||||||
onLayout={coverLayout.onLayout}
|
onLayout={coverLayout.onLayout}
|
||||||
@ -73,17 +67,18 @@ const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
|
|||||||
style={styles.scroll}
|
style={styles.scroll}
|
||||||
contentContainerStyle={styles.scrollContent}>
|
contentContainerStyle={styles.scrollContent}>
|
||||||
<CoverArt
|
<CoverArt
|
||||||
FallbackComponent={ArtistCoverFallback}
|
artistId={artist.id}
|
||||||
coverArtUri={artist.largeImageUrl}
|
|
||||||
style={styles.artistCover}
|
style={styles.artistCover}
|
||||||
resizeMode={FastImage.resizeMode.cover}
|
resizeMode={FastImage.resizeMode.cover}
|
||||||
|
round={false}
|
||||||
|
imageSize="original"
|
||||||
/>
|
/>
|
||||||
<View style={styles.titleContainer}>
|
<View style={styles.titleContainer}>
|
||||||
<Text style={styles.title}>{artist.name}</Text>
|
<Text style={styles.title}>{artist.name}</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{artist.topSongs.length > 0 ? <TopSongs /> : <></>}
|
{artist.topSongs.length > 0 ? <TopSongs /> : <></>}
|
||||||
<Text style={styles.header}>Albums</Text>
|
<Header>Albums</Header>
|
||||||
<View style={styles.albums} onLayout={albumsLayout.onLayout}>
|
<View style={styles.albums} onLayout={albumsLayout.onLayout}>
|
||||||
{artist.albums.map(a => (
|
{artist.albums.map(a => (
|
||||||
<AlbumItem key={a.id} album={a} height={albumSize} width={albumSize} />
|
<AlbumItem key={a.id} album={a} height={albumSize} width={albumSize} />
|
||||||
@ -140,13 +135,6 @@ const styles = StyleSheet.create({
|
|||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
marginBottom: 10,
|
marginBottom: 10,
|
||||||
},
|
},
|
||||||
header: {
|
|
||||||
fontFamily: font.bold,
|
|
||||||
fontSize: 24,
|
|
||||||
color: colors.text.primary,
|
|
||||||
marginTop: 20,
|
|
||||||
marginBottom: 14,
|
|
||||||
},
|
|
||||||
artistCover: {
|
artistCover: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
height: artistCoverHeight,
|
height: artistCoverHeight,
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import CoverArt from '@app/components/CoverArt'
|
import CoverArt from '@app/components/CoverArt'
|
||||||
import GradientScrollView from '@app/components/GradientScrollView'
|
import GradientScrollView from '@app/components/GradientScrollView'
|
||||||
|
import Header from '@app/components/Header'
|
||||||
import NothingHere from '@app/components/NothingHere'
|
import NothingHere from '@app/components/NothingHere'
|
||||||
import PressableOpacity from '@app/components/PressableOpacity'
|
import PressableOpacity from '@app/components/PressableOpacity'
|
||||||
import { AlbumListItem } from '@app/models/music'
|
import { AlbumListItem } from '@app/models/music'
|
||||||
@ -30,7 +31,7 @@ const AlbumItem = React.memo<{
|
|||||||
onPress={() => navigation.navigate('AlbumView', { id: album.id, title: album.name })}
|
onPress={() => navigation.navigate('AlbumView', { id: album.id, title: album.name })}
|
||||||
key={album.id}
|
key={album.id}
|
||||||
style={styles.item}>
|
style={styles.item}>
|
||||||
<CoverArt coverArtUri={album.coverArtThumbUri} style={{ height: styles.item.width, width: styles.item.width }} />
|
<CoverArt coverArt={album.coverArt} style={{ height: styles.item.width, width: styles.item.width }} />
|
||||||
<Text style={styles.title} numberOfLines={1}>
|
<Text style={styles.title} numberOfLines={1}>
|
||||||
{album.name}
|
{album.name}
|
||||||
</Text>
|
</Text>
|
||||||
@ -66,7 +67,7 @@ const Category = React.memo<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.category}>
|
<View style={styles.category}>
|
||||||
<Text style={styles.categoryHeader}>{name}</Text>
|
<Header style={styles.header}>{name}</Header>
|
||||||
{data.length > 0 ? <Albums /> : <Nothing />}
|
{data.length > 0 ? <Albums /> : <Nothing />}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@ -111,24 +112,19 @@ const styles = StyleSheet.create({
|
|||||||
content: {
|
content: {
|
||||||
paddingBottom: 20,
|
paddingBottom: 20,
|
||||||
},
|
},
|
||||||
category: {
|
header: {
|
||||||
marginTop: 12,
|
|
||||||
},
|
|
||||||
categoryHeader: {
|
|
||||||
fontFamily: font.bold,
|
|
||||||
fontSize: 24,
|
|
||||||
color: colors.text.primary,
|
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
marginTop: 4,
|
},
|
||||||
|
category: {
|
||||||
|
// marginTop: 12,
|
||||||
},
|
},
|
||||||
nothingHereContent: {
|
nothingHereContent: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 200,
|
height: 190,
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
artScroll: {
|
artScroll: {
|
||||||
marginTop: 10,
|
|
||||||
height: 190,
|
height: 190,
|
||||||
},
|
},
|
||||||
artScrollContent: {
|
artScrollContent: {
|
||||||
|
|||||||
@ -17,15 +17,15 @@ const AlbumItem = React.memo<{
|
|||||||
size: number
|
size: number
|
||||||
height: number
|
height: number
|
||||||
artist?: string
|
artist?: string
|
||||||
coverArtUri?: string
|
coverArt?: string
|
||||||
}>(({ id, name, artist, size, height, coverArtUri }) => {
|
}>(({ id, name, artist, size, height, coverArt }) => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PressableOpacity
|
<PressableOpacity
|
||||||
style={[styles.item, { maxWidth: size, height }]}
|
style={[styles.item, { maxWidth: size, height }]}
|
||||||
onPress={() => navigation.navigate('AlbumView', { id, title: name })}>
|
onPress={() => navigation.navigate('AlbumView', { id, title: name })}>
|
||||||
<CoverArt coverArtUri={coverArtUri} style={{ height: size, width: size }} />
|
<CoverArt coverArt={coverArt} style={{ height: size, width: size }} />
|
||||||
<View style={styles.itemDetails}>
|
<View style={styles.itemDetails}>
|
||||||
<Text style={styles.title} numberOfLines={1}>
|
<Text style={styles.title} numberOfLines={1}>
|
||||||
{name}
|
{name}
|
||||||
@ -43,7 +43,7 @@ const AlbumListRenderItem: React.FC<{
|
|||||||
}> = ({ item }) => (
|
}> = ({ item }) => (
|
||||||
<AlbumItem
|
<AlbumItem
|
||||||
id={item.album.id}
|
id={item.album.id}
|
||||||
coverArtUri={item.album.coverArtThumbUri}
|
coverArt={item.album.coverArt}
|
||||||
name={item.album.name}
|
name={item.album.name}
|
||||||
artist={item.album.artist}
|
artist={item.album.artist}
|
||||||
size={item.size}
|
size={item.size}
|
||||||
|
|||||||
@ -1,25 +1,23 @@
|
|||||||
import ArtistArt from '@app/components/ArtistArt'
|
|
||||||
import GradientFlatList from '@app/components/GradientFlatList'
|
import GradientFlatList from '@app/components/GradientFlatList'
|
||||||
import PressableOpacity from '@app/components/PressableOpacity'
|
import ListItem from '@app/components/ListItem'
|
||||||
import { Artist } from '@app/models/music'
|
import { Artist } from '@app/models/music'
|
||||||
import { artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '@app/state/music'
|
import { artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '@app/state/music'
|
||||||
import colors from '@app/styles/colors'
|
|
||||||
import font from '@app/styles/font'
|
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { useAtomValue } from 'jotai/utils'
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { StyleSheet, Text } from 'react-native'
|
import { StyleSheet } from 'react-native'
|
||||||
|
|
||||||
const ArtistItem = React.memo<{ item: Artist }>(({ item }) => {
|
const ArtistItem = React.memo<{ item: Artist }>(({ item }) => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PressableOpacity
|
<ListItem
|
||||||
style={styles.item}
|
item={item}
|
||||||
onPress={() => navigation.navigate('ArtistView', { id: item.id, title: item.name })}>
|
showArt={true}
|
||||||
<ArtistArt id={item.id} width={styles.art.width} height={styles.art.height} />
|
showStar={false}
|
||||||
<Text style={styles.title}>{item.name}</Text>
|
listStyle="big"
|
||||||
</PressableOpacity>
|
onPress={() => navigation.navigate('ArtistView', { id: item.id, title: item.name })}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -52,23 +50,8 @@ const ArtistsList = () => {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
listContent: {
|
listContent: {
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
},
|
paddingHorizontal: 10,
|
||||||
item: {
|
paddingTop: 6,
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
marginVertical: 6,
|
|
||||||
marginLeft: 10,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontFamily: font.semiBold,
|
|
||||||
fontSize: 16,
|
|
||||||
color: colors.text.primary,
|
|
||||||
marginLeft: 10,
|
|
||||||
},
|
|
||||||
art: {
|
|
||||||
height: 70,
|
|
||||||
width: 70,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -1,36 +1,24 @@
|
|||||||
import CoverArt from '@app/components/CoverArt'
|
|
||||||
import GradientFlatList from '@app/components/GradientFlatList'
|
import GradientFlatList from '@app/components/GradientFlatList'
|
||||||
import PressableOpacity from '@app/components/PressableOpacity'
|
import ListItem from '@app/components/ListItem'
|
||||||
import { PlaylistListItem } from '@app/models/music'
|
import { PlaylistListItem } from '@app/models/music'
|
||||||
import { playlistsAtom, playlistsUpdatingAtom, useUpdatePlaylists } from '@app/state/music'
|
import { playlistsAtom, playlistsUpdatingAtom, useUpdatePlaylists } from '@app/state/music'
|
||||||
import colors from '@app/styles/colors'
|
|
||||||
import font from '@app/styles/font'
|
|
||||||
import { useNavigation } from '@react-navigation/native'
|
import { useNavigation } from '@react-navigation/native'
|
||||||
import { useAtomValue } from 'jotai/utils'
|
import { useAtomValue } from 'jotai/utils'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { StyleSheet, Text, View } from 'react-native'
|
import { StyleSheet } from 'react-native'
|
||||||
|
|
||||||
const PlaylistItem = React.memo<{ item: PlaylistListItem }>(({ item }) => {
|
const PlaylistItem = React.memo<{ item: PlaylistListItem }>(({ item }) => {
|
||||||
const navigation = useNavigation()
|
const navigation = useNavigation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PressableOpacity
|
<ListItem
|
||||||
style={styles.item}
|
item={item}
|
||||||
onPress={() => navigation.navigate('PlaylistView', { id: item.id, title: item.name })}>
|
showArt={true}
|
||||||
<CoverArt coverArtUri={item.coverArtThumbUri} style={styles.art} />
|
showStar={false}
|
||||||
<View style={styles.text}>
|
listStyle="big"
|
||||||
<Text style={styles.title} numberOfLines={1}>
|
subtitle={item.comment}
|
||||||
{item.name}
|
onPress={() => navigation.navigate('PlaylistView', { id: item.id, title: item.name })}
|
||||||
</Text>
|
/>
|
||||||
{item.comment ? (
|
|
||||||
<Text style={styles.subtitle} numberOfLines={1}>
|
|
||||||
{item.comment}
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</PressableOpacity>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -63,30 +51,8 @@ const PlaylistsList = () => {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
listContent: {
|
listContent: {
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
},
|
paddingHorizontal: 10,
|
||||||
item: {
|
paddingTop: 6,
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'flex-start',
|
|
||||||
marginVertical: 6,
|
|
||||||
marginLeft: 10,
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
marginLeft: 10,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontFamily: font.semiBold,
|
|
||||||
fontSize: 16,
|
|
||||||
color: colors.text.primary,
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
fontFamily: font.regular,
|
|
||||||
fontSize: 14,
|
|
||||||
color: colors.text.secondary,
|
|
||||||
},
|
|
||||||
art: {
|
|
||||||
height: 70,
|
|
||||||
width: 70,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -75,7 +75,7 @@ const SongCoverArt = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={coverArtStyles.container}>
|
<View style={coverArtStyles.container}>
|
||||||
<CoverArt coverArtUri={track?.artwork as string} style={coverArtStyles.image} />
|
<CoverArt coverArt={track?.coverArt} style={coverArtStyles.image} imageSize="original" />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -318,7 +318,7 @@ const NowPlayingLayout: React.FC<NowPlayingProps> = ({ navigation }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<ImageGradientBackground imageUri={track?.artworkThumb as string} imageKey={`${track?.album}${track?.artist}`} />
|
<ImageGradientBackground imageUri={track?.artwork as string} imageKey={`${track?.album}${track?.artist}`} />
|
||||||
<NowPlayingHeader />
|
<NowPlayingHeader />
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<SongCoverArt />
|
<SongCoverArt />
|
||||||
|
|||||||
@ -3,8 +3,8 @@ import GradientBackground from '@app/components/GradientBackground'
|
|||||||
import ImageGradientScrollView from '@app/components/ImageGradientScrollView'
|
import ImageGradientScrollView from '@app/components/ImageGradientScrollView'
|
||||||
import ListPlayerControls from '@app/components/ListPlayerControls'
|
import ListPlayerControls from '@app/components/ListPlayerControls'
|
||||||
import NothingHere from '@app/components/NothingHere'
|
import NothingHere from '@app/components/NothingHere'
|
||||||
import SongItem from '@app/components/SongItem'
|
import ListItem from '@app/components/ListItem'
|
||||||
import { playlistAtomFamily } from '@app/state/music'
|
import { playlistAtomFamily, useCoverArtUri } from '@app/state/music'
|
||||||
import { useSetQueue } from '@app/state/trackplayer'
|
import { useSetQueue } from '@app/state/trackplayer'
|
||||||
import colors from '@app/styles/colors'
|
import colors from '@app/styles/colors'
|
||||||
import font from '@app/styles/font'
|
import font from '@app/styles/font'
|
||||||
@ -18,6 +18,7 @@ const PlaylistDetails: React.FC<{
|
|||||||
}> = ({ id }) => {
|
}> = ({ id }) => {
|
||||||
const playlist = useAtomValue(playlistAtomFamily(id))
|
const playlist = useAtomValue(playlistAtomFamily(id))
|
||||||
const setQueue = useSetQueue()
|
const setQueue = useSetQueue()
|
||||||
|
const coverArtUri = useCoverArtUri()
|
||||||
|
|
||||||
if (!playlist) {
|
if (!playlist) {
|
||||||
return <></>
|
return <></>
|
||||||
@ -33,7 +34,13 @@ const PlaylistDetails: React.FC<{
|
|||||||
/>
|
/>
|
||||||
<View style={styles.songs}>
|
<View style={styles.songs}>
|
||||||
{playlist.songs.map((s, i) => (
|
{playlist.songs.map((s, i) => (
|
||||||
<SongItem key={i} song={s} showArt={true} onPress={() => setQueue(playlist.songs, playlist.name, i)} />
|
<ListItem
|
||||||
|
key={i}
|
||||||
|
item={s}
|
||||||
|
subtitle={s.artist}
|
||||||
|
showArt={true}
|
||||||
|
onPress={() => setQueue(playlist.songs, playlist.name, i)}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
</>
|
</>
|
||||||
@ -41,11 +48,11 @@ const PlaylistDetails: React.FC<{
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ImageGradientScrollView
|
<ImageGradientScrollView
|
||||||
imageUri={playlist.coverArtThumbUri}
|
imageUri={coverArtUri(playlist.coverArt)}
|
||||||
imageKey={`${playlist.id}${playlist.name}`}
|
imageKey={`${playlist.id}${playlist.name}`}
|
||||||
style={styles.container}>
|
style={styles.container}>
|
||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<CoverArt coverArtUri={playlist.coverArtUri} style={styles.cover} />
|
<CoverArt coverArt={playlist.coverArt} style={styles.cover} imageSize="original" />
|
||||||
<Text style={styles.title}>{playlist.name}</Text>
|
<Text style={styles.title}>{playlist.name}</Text>
|
||||||
{playlist.comment ? <Text style={styles.subtitle}>{playlist.comment}</Text> : <></>}
|
{playlist.comment ? <Text style={styles.subtitle}>{playlist.comment}</Text> : <></>}
|
||||||
{playlist.songs.length > 0 ? <Songs /> : <NothingHere height={350} width={250} />}
|
{playlist.songs.length > 0 ? <Songs /> : <NothingHere height={350} width={250} />}
|
||||||
|
|||||||
75
app/screens/Search.tsx
Normal file
75
app/screens/Search.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import GradientScrollView from '@app/components/GradientScrollView'
|
||||||
|
import Header from '@app/components/Header'
|
||||||
|
import ListItem from '@app/components/ListItem'
|
||||||
|
import { searchResultsAtom, useUpdateSearchResults } from '@app/state/music'
|
||||||
|
import colors from '@app/styles/colors'
|
||||||
|
import font from '@app/styles/font'
|
||||||
|
import { useAtomValue } from 'jotai/utils'
|
||||||
|
import React, { useState } from 'react'
|
||||||
|
import { StatusBar, StyleSheet, View, TextInput } from 'react-native'
|
||||||
|
|
||||||
|
const Search = () => {
|
||||||
|
const [text, setText] = useState('')
|
||||||
|
const updateSearch = useUpdateSearchResults()
|
||||||
|
const results = useAtomValue(searchResultsAtom)
|
||||||
|
|
||||||
|
const onSubmitEditing = () => {
|
||||||
|
console.log(text)
|
||||||
|
updateSearch(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GradientScrollView style={styles.scroll} contentContainerStyle={styles.scrollContentContainer}>
|
||||||
|
<View style={styles.content}>
|
||||||
|
<TextInput
|
||||||
|
style={styles.textInput}
|
||||||
|
placeholder="Search"
|
||||||
|
placeholderTextColor="grey"
|
||||||
|
selectionColor={colors.text.secondary}
|
||||||
|
value={text}
|
||||||
|
onChangeText={setText}
|
||||||
|
onSubmitEditing={onSubmitEditing}
|
||||||
|
/>
|
||||||
|
<Header>Artists</Header>
|
||||||
|
{results.artists.map(a => (
|
||||||
|
<ListItem key={a.id} item={a} showArt={true} showStar={false} />
|
||||||
|
))}
|
||||||
|
<Header>Albums</Header>
|
||||||
|
{results.albums.map(a => (
|
||||||
|
<ListItem key={a.id} item={a} showArt={true} showStar={false} subtitle={a.artist} />
|
||||||
|
))}
|
||||||
|
<Header>Songs</Header>
|
||||||
|
{results.songs.map(a => (
|
||||||
|
<ListItem key={a.id} item={a} showArt={true} showStar={false} subtitle={a.artist} />
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</GradientScrollView>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
scroll: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
scrollContentContainer: {
|
||||||
|
paddingTop: StatusBar.currentHeight,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
},
|
||||||
|
textInput: {
|
||||||
|
backgroundColor: '#515151',
|
||||||
|
fontFamily: font.regular,
|
||||||
|
fontSize: 18,
|
||||||
|
color: colors.text.primary,
|
||||||
|
marginTop: 20,
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
},
|
||||||
|
itemText: {
|
||||||
|
color: colors.text.primary,
|
||||||
|
fontFamily: font.regular,
|
||||||
|
fontSize: 14,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default Search
|
||||||
@ -3,10 +3,10 @@ import {
|
|||||||
AlbumListItem,
|
AlbumListItem,
|
||||||
AlbumWithSongs,
|
AlbumWithSongs,
|
||||||
Artist,
|
Artist,
|
||||||
ArtistArt,
|
|
||||||
ArtistInfo,
|
ArtistInfo,
|
||||||
PlaylistListItem,
|
PlaylistListItem,
|
||||||
PlaylistWithSongs,
|
PlaylistWithSongs,
|
||||||
|
SearchResults,
|
||||||
Song,
|
Song,
|
||||||
} from '@app/models/music'
|
} from '@app/models/music'
|
||||||
import { activeServerAtom, homeListTypesAtom } from '@app/state/settings'
|
import { activeServerAtom, homeListTypesAtom } from '@app/state/settings'
|
||||||
@ -19,7 +19,7 @@ import {
|
|||||||
PlaylistElement,
|
PlaylistElement,
|
||||||
PlaylistWithSongsElement,
|
PlaylistWithSongsElement,
|
||||||
} from '@app/subsonic/elements'
|
} from '@app/subsonic/elements'
|
||||||
import { GetAlbumList2Type } from '@app/subsonic/params'
|
import { GetAlbumList2Type, GetCoverArtParams } from '@app/subsonic/params'
|
||||||
import { GetArtistResponse } from '@app/subsonic/responses'
|
import { GetArtistResponse } from '@app/subsonic/responses'
|
||||||
import { atom, useAtom } from 'jotai'
|
import { atom, useAtom } from 'jotai'
|
||||||
import { atomFamily, useAtomValue, useUpdateAtom } from 'jotai/utils'
|
import { atomFamily, useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||||
@ -87,7 +87,7 @@ export const useUpdateHomeLists = () => {
|
|||||||
for (const type of types) {
|
for (const type of types) {
|
||||||
promises.push(
|
promises.push(
|
||||||
client.getAlbumList2({ type: type as GetAlbumList2Type, size: 20 }).then(response => {
|
client.getAlbumList2({ type: type as GetAlbumList2Type, size: 20 }).then(response => {
|
||||||
updateHomeList({ type, albums: response.data.albums.map(a => mapAlbumID3toAlbumListItem(a, client)) })
|
updateHomeList({ type, albums: response.data.albums.map(mapAlbumID3toAlbumListItem) })
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -97,6 +97,40 @@ export const useUpdateHomeLists = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const searchResultsUpdatingAtom = atom(false)
|
||||||
|
export const searchResultsAtom = atom<SearchResults>({
|
||||||
|
artists: [],
|
||||||
|
albums: [],
|
||||||
|
songs: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const useUpdateSearchResults = () => {
|
||||||
|
const server = useAtomValue(activeServerAtom)
|
||||||
|
const updateList = useUpdateAtom(searchResultsAtom)
|
||||||
|
const [updating, setUpdating] = useAtom(searchResultsUpdatingAtom)
|
||||||
|
|
||||||
|
if (!server) {
|
||||||
|
return async () => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return async (query: string) => {
|
||||||
|
if (updating) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setUpdating(true)
|
||||||
|
|
||||||
|
const client = new SubsonicApiClient(server)
|
||||||
|
const response = await client.search3({ query })
|
||||||
|
|
||||||
|
updateList({
|
||||||
|
artists: response.data.artists.map(mapArtistID3toArtist),
|
||||||
|
albums: response.data.albums.map(mapAlbumID3toAlbumListItem),
|
||||||
|
songs: response.data.songs.map(a => mapChildToSong(a, client)),
|
||||||
|
})
|
||||||
|
setUpdating(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const playlistsUpdatingAtom = atom(false)
|
export const playlistsUpdatingAtom = atom(false)
|
||||||
export const playlistsAtom = atom<PlaylistListItem[]>([])
|
export const playlistsAtom = atom<PlaylistListItem[]>([])
|
||||||
|
|
||||||
@ -118,7 +152,7 @@ export const useUpdatePlaylists = () => {
|
|||||||
const client = new SubsonicApiClient(server)
|
const client = new SubsonicApiClient(server)
|
||||||
const response = await client.getPlaylists()
|
const response = await client.getPlaylists()
|
||||||
|
|
||||||
updateList(response.data.playlists.map(a => mapPlaylistListItem(a, client)))
|
updateList(response.data.playlists.map(mapPlaylistListItem))
|
||||||
setUpdating(false)
|
setUpdating(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,7 +191,7 @@ export const useUpdateAlbumList = () => {
|
|||||||
const client = new SubsonicApiClient(server)
|
const client = new SubsonicApiClient(server)
|
||||||
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 500 })
|
const response = await client.getAlbumList2({ type: 'alphabeticalByArtist', size: 500 })
|
||||||
|
|
||||||
updateList(response.data.albums.map(a => mapAlbumID3toAlbumListItem(a, client)))
|
updateList(response.data.albums.map(mapAlbumID3toAlbumListItem))
|
||||||
setUpdating(false)
|
setUpdating(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -193,36 +227,32 @@ export const artistInfoAtomFamily = atomFamily((id: string) =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
export const artistArtAtomFamily = atomFamily((id: string) =>
|
export const useCoverArtUri = () => {
|
||||||
atom<ArtistArt | undefined>(async get => {
|
const server = useAtomValue(activeServerAtom)
|
||||||
const artistInfo = get(artistInfoAtomFamily(id))
|
|
||||||
if (!artistInfo) {
|
if (!server) {
|
||||||
return undefined
|
return () => undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const albumCoverUris = artistInfo.albums
|
const client = new SubsonicApiClient(server)
|
||||||
.filter(a => a.coverArtThumbUri !== undefined)
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (b.year && a.year) {
|
|
||||||
return b.year - a.year
|
|
||||||
} else {
|
|
||||||
return a.name.localeCompare(b.name)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map(a => a.coverArtThumbUri) as string[]
|
|
||||||
|
|
||||||
return {
|
return (coverArt?: string, size: 'thumbnail' | 'original' = 'thumbnail') => {
|
||||||
albumCoverUris,
|
const params: GetCoverArtParams = { id: coverArt || '-1' }
|
||||||
uri: artistInfo.largeImageUrl,
|
if (size === 'thumbnail') {
|
||||||
|
params.size = '256'
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
)
|
return client.getCoverArtUri(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function mapArtistID3toArtist(artist: ArtistID3Element): Artist {
|
function mapArtistID3toArtist(artist: ArtistID3Element): Artist {
|
||||||
return {
|
return {
|
||||||
|
itemType: 'artist',
|
||||||
id: artist.id,
|
id: artist.id,
|
||||||
name: artist.name,
|
name: artist.name,
|
||||||
starred: artist.starred,
|
starred: artist.starred,
|
||||||
|
coverArt: artist.coverArt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,63 +264,40 @@ function mapArtistInfo(
|
|||||||
): ArtistInfo {
|
): ArtistInfo {
|
||||||
const { artist, albums } = artistResponse
|
const { artist, albums } = artistResponse
|
||||||
|
|
||||||
const mappedAlbums = albums.map(a => mapAlbumID3toAlbum(a, client))
|
const mappedAlbums = albums.map(mapAlbumID3toAlbum)
|
||||||
const albumCoverUris = mappedAlbums
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (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 {
|
return {
|
||||||
...mapArtistID3toArtist(artist),
|
...mapArtistID3toArtist(artist),
|
||||||
albums: mappedAlbums,
|
albums: mappedAlbums,
|
||||||
albumCoverUris,
|
smallImageUrl: info.smallImageUrl,
|
||||||
mediumImageUrl: info.mediumImageUrl,
|
mediumImageUrl: info.mediumImageUrl,
|
||||||
largeImageUrl: info.largeImageUrl,
|
largeImageUrl: info.largeImageUrl,
|
||||||
topSongs: topSongs.map(c => mapChildToSong(c, client)).slice(0, 5),
|
topSongs: topSongs.map(s => mapChildToSong(s, client)).slice(0, 5),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapCoverArtUri(item: { coverArt?: string }, client: SubsonicApiClient) {
|
function mapAlbumID3toAlbumListItem(album: AlbumID3Element): AlbumListItem {
|
||||||
return {
|
|
||||||
coverArtUri: item.coverArt ? client.getCoverArtUri({ id: item.coverArt }) : client.getCoverArtUri({ id: '-1' }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapCoverArtThumbUri(item: { coverArt?: string }, client: SubsonicApiClient) {
|
|
||||||
return {
|
|
||||||
coverArtThumbUri: item.coverArt
|
|
||||||
? client.getCoverArtUri({ id: item.coverArt, size: '256' })
|
|
||||||
: client.getCoverArtUri({ id: '-1', size: '256' }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapAlbumID3toAlbumListItem(album: AlbumID3Element, client: SubsonicApiClient): AlbumListItem {
|
|
||||||
return {
|
return {
|
||||||
|
itemType: 'album',
|
||||||
id: album.id,
|
id: album.id,
|
||||||
name: album.name,
|
name: album.name,
|
||||||
artist: album.artist,
|
artist: album.artist,
|
||||||
starred: album.starred,
|
starred: album.starred,
|
||||||
...mapCoverArtThumbUri(album, client),
|
coverArt: album.coverArt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapAlbumID3toAlbum(album: AlbumID3Element, client: SubsonicApiClient): Album {
|
function mapAlbumID3toAlbum(album: AlbumID3Element): Album {
|
||||||
return {
|
return {
|
||||||
...mapAlbumID3toAlbumListItem(album, client),
|
...mapAlbumID3toAlbumListItem(album),
|
||||||
...mapCoverArtUri(album, client),
|
coverArt: album.coverArt,
|
||||||
...mapCoverArtThumbUri(album, client),
|
|
||||||
year: album.year,
|
year: album.year,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
|
function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
|
||||||
return {
|
return {
|
||||||
|
itemType: 'song',
|
||||||
id: child.id,
|
id: child.id,
|
||||||
album: child.album,
|
album: child.album,
|
||||||
artist: child.artist,
|
artist: child.artist,
|
||||||
@ -298,9 +305,8 @@ function mapChildToSong(child: ChildElement, client: SubsonicApiClient): Song {
|
|||||||
track: child.track,
|
track: child.track,
|
||||||
duration: child.duration,
|
duration: child.duration,
|
||||||
starred: child.starred,
|
starred: child.starred,
|
||||||
|
coverArt: child.coverArt,
|
||||||
streamUri: client.streamUri({ id: child.id }),
|
streamUri: client.streamUri({ id: child.id }),
|
||||||
...mapCoverArtUri(child, client),
|
|
||||||
...mapCoverArtThumbUri(child, client),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,24 +316,25 @@ function mapAlbumID3WithSongstoAlbunWithSongs(
|
|||||||
client: SubsonicApiClient,
|
client: SubsonicApiClient,
|
||||||
): AlbumWithSongs {
|
): AlbumWithSongs {
|
||||||
return {
|
return {
|
||||||
...mapAlbumID3toAlbum(album, client),
|
...mapAlbumID3toAlbum(album),
|
||||||
songs: songs.map(s => mapChildToSong(s, client)),
|
songs: songs.map(s => mapChildToSong(s, client)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapPlaylistListItem(playlist: PlaylistElement, client: SubsonicApiClient): PlaylistListItem {
|
function mapPlaylistListItem(playlist: PlaylistElement): PlaylistListItem {
|
||||||
return {
|
return {
|
||||||
|
itemType: 'playlist',
|
||||||
id: playlist.id,
|
id: playlist.id,
|
||||||
name: playlist.name,
|
name: playlist.name,
|
||||||
comment: playlist.comment,
|
comment: playlist.comment,
|
||||||
...mapCoverArtThumbUri(playlist, client),
|
coverArt: playlist.coverArt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapPlaylistWithSongs(playlist: PlaylistWithSongsElement, client: SubsonicApiClient): PlaylistWithSongs {
|
function mapPlaylistWithSongs(playlist: PlaylistWithSongsElement, client: SubsonicApiClient): PlaylistWithSongs {
|
||||||
return {
|
return {
|
||||||
...mapPlaylistListItem(playlist, client),
|
...mapPlaylistListItem(playlist),
|
||||||
songs: playlist.songs.map(s => mapChildToSong(s, client)),
|
songs: playlist.songs.map(s => mapChildToSong(s, client)),
|
||||||
...mapCoverArtUri(playlist, client),
|
coverArt: playlist.coverArt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,10 +6,11 @@ import { atom } from 'jotai'
|
|||||||
import { useAtomCallback, useAtomValue, useUpdateAtom } from 'jotai/utils'
|
import { useAtomCallback, useAtomValue, useUpdateAtom } from 'jotai/utils'
|
||||||
import { useCallback, useEffect } from 'react'
|
import { useCallback, useEffect } from 'react'
|
||||||
import TrackPlayer, { State, Track } from 'react-native-track-player'
|
import TrackPlayer, { State, Track } from 'react-native-track-player'
|
||||||
|
import { useCoverArtUri } from './music'
|
||||||
|
|
||||||
type TrackExt = Track & {
|
type TrackExt = Track & {
|
||||||
id: string
|
id: string
|
||||||
artworkThumb?: string
|
coverArt?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionalTrackExt = TrackExt | undefined
|
type OptionalTrackExt = TrackExt | undefined
|
||||||
@ -316,6 +317,7 @@ export const useSetQueue = () => {
|
|||||||
const setQueueName = useUpdateAtom(queueNameWriteAtom)
|
const setQueueName = useUpdateAtom(queueNameWriteAtom)
|
||||||
const reset = useReset(false)
|
const reset = useReset(false)
|
||||||
const getQueueShuffled = useAtomCallback(useCallback(get => get(queueShuffledAtom), []))
|
const getQueueShuffled = useAtomCallback(useCallback(get => get(queueShuffledAtom), []))
|
||||||
|
const coverArtUri = useCoverArtUri()
|
||||||
|
|
||||||
return async (songs: Song[], name: string, playTrack?: number, shuffle?: boolean) =>
|
return async (songs: Song[], name: string, playTrack?: number, shuffle?: boolean) =>
|
||||||
trackPlayerCommands.enqueue(async () => {
|
trackPlayerCommands.enqueue(async () => {
|
||||||
@ -328,7 +330,7 @@ export const useSetQueue = () => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let queue = songs.map(mapSongToTrack)
|
let queue = songs.map(s => mapSongToTrack(s, coverArtUri))
|
||||||
|
|
||||||
if (shuffled) {
|
if (shuffled) {
|
||||||
const { tracks, shuffleOrder } = shuffleTracks(queue, playTrack)
|
const { tracks, shuffleOrder } = shuffleTracks(queue, playTrack)
|
||||||
@ -371,15 +373,15 @@ export const useProgress = () => {
|
|||||||
return progress
|
return progress
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapSongToTrack(song: Song): TrackExt {
|
function mapSongToTrack(song: Song, coverArtUri: (coverArt?: string) => string | undefined): TrackExt {
|
||||||
return {
|
return {
|
||||||
id: song.id,
|
id: song.id,
|
||||||
title: song.title,
|
title: song.title,
|
||||||
artist: song.artist || 'Unknown Artist',
|
artist: song.artist || 'Unknown Artist',
|
||||||
album: song.album || 'Unknown Album',
|
album: song.album || 'Unknown Album',
|
||||||
url: song.streamUri,
|
url: song.streamUri,
|
||||||
artwork: song.coverArtUri,
|
artwork: coverArtUri(song.coverArt),
|
||||||
artworkThumb: song.coverArtThumbUri,
|
coverArt: song.coverArt,
|
||||||
duration: song.duration,
|
duration: song.duration,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
GetPlaylistParams,
|
GetPlaylistParams,
|
||||||
GetPlaylistsParams,
|
GetPlaylistsParams,
|
||||||
GetTopSongsParams,
|
GetTopSongsParams,
|
||||||
|
Search3Params,
|
||||||
StreamParams,
|
StreamParams,
|
||||||
} from '@app/subsonic/params'
|
} from '@app/subsonic/params'
|
||||||
import {
|
import {
|
||||||
@ -28,6 +29,7 @@ import {
|
|||||||
GetPlaylistResponse,
|
GetPlaylistResponse,
|
||||||
GetPlaylistsResponse,
|
GetPlaylistsResponse,
|
||||||
GetTopSongsResponse,
|
GetTopSongsResponse,
|
||||||
|
Search3Response,
|
||||||
SubsonicResponse,
|
SubsonicResponse,
|
||||||
} from '@app/subsonic/responses'
|
} from '@app/subsonic/responses'
|
||||||
import { Server } from '@app/models/settings'
|
import { Server } from '@app/models/settings'
|
||||||
@ -220,4 +222,13 @@ export class SubsonicApiClient {
|
|||||||
streamUri(params: StreamParams): string {
|
streamUri(params: StreamParams): string {
|
||||||
return this.buildUrl('stream', params)
|
return this.buildUrl('stream', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Searching
|
||||||
|
//
|
||||||
|
|
||||||
|
async search3(params: Search3Params): Promise<SubsonicResponse<Search3Response>> {
|
||||||
|
const xml = await this.apiGetXml('search3', params)
|
||||||
|
return new SubsonicResponse<Search3Response>(xml, new Search3Response(xml))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -99,3 +99,18 @@ export type StreamParams = {
|
|||||||
format?: string
|
format?: string
|
||||||
estimateContentLength?: boolean
|
estimateContentLength?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Searching
|
||||||
|
//
|
||||||
|
|
||||||
|
export type Search3Params = {
|
||||||
|
query: string
|
||||||
|
artistCount?: number
|
||||||
|
artistOffset?: number
|
||||||
|
albumCount?: number
|
||||||
|
albumOffset?: number
|
||||||
|
songCount?: number
|
||||||
|
songOffset?: number
|
||||||
|
musicFolderId?: string
|
||||||
|
}
|
||||||
|
|||||||
@ -178,3 +178,30 @@ export class GetPlaylistResponse {
|
|||||||
this.playlist = new PlaylistWithSongsElement(xml.getElementsByTagName('playlist')[0])
|
this.playlist = new PlaylistWithSongsElement(xml.getElementsByTagName('playlist')[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Searching
|
||||||
|
//
|
||||||
|
|
||||||
|
export class Search3Response {
|
||||||
|
artists: ArtistID3Element[] = []
|
||||||
|
albums: AlbumID3Element[] = []
|
||||||
|
songs: ChildElement[] = []
|
||||||
|
|
||||||
|
constructor(xml: Document) {
|
||||||
|
const artistElements = xml.getElementsByTagName('artist')
|
||||||
|
for (let i = 0; i < artistElements.length; i++) {
|
||||||
|
this.artists.push(new ArtistID3Element(artistElements[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const albumElements = xml.getElementsByTagName('album')
|
||||||
|
for (let i = 0; i < albumElements.length; i++) {
|
||||||
|
this.albums.push(new AlbumID3Element(albumElements[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
const songElements = xml.getElementsByTagName('song')
|
||||||
|
for (let i = 0; i < songElements.length; i++) {
|
||||||
|
this.songs.push(new ChildElement(songElements[i]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user