mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 06:52:43 +01:00
full reworked images to download (cache) first
This commit is contained in:
@@ -1,146 +1,92 @@
|
||||
import { useArtistInfo, useCoverArtUri } from '@app/hooks/music'
|
||||
import { useArtistCoverArtFile, useCoverArtFile } from '@app/hooks/music'
|
||||
import { DownloadFile } from '@app/state/music'
|
||||
import colors from '@app/styles/colors'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { ActivityIndicator, StyleSheet, View, ViewStyle } from 'react-native'
|
||||
import FastImage, { ImageStyle } from 'react-native-fast-image'
|
||||
|
||||
type BaseProps = {
|
||||
imageSize?: 'thumbnail' | 'original'
|
||||
style?: ViewStyle
|
||||
imageStyle?: ImageStyle
|
||||
resizeMode?: keyof typeof FastImage.resizeMode
|
||||
round?: boolean
|
||||
}
|
||||
|
||||
type BaseImageProps = BaseProps & {
|
||||
enableLoading: () => void
|
||||
disableLoading: () => void
|
||||
fallbackError: () => void
|
||||
}
|
||||
|
||||
type ArtistIdProp = {
|
||||
type ArtistCoverArtProps = BaseProps & {
|
||||
type: 'artist'
|
||||
artistId: string
|
||||
}
|
||||
|
||||
type CoverArtProp = {
|
||||
type CoverArtProps = BaseProps & {
|
||||
type: 'cover'
|
||||
coverArt?: string
|
||||
}
|
||||
|
||||
type ArtistIdImageProps = BaseImageProps & ArtistIdProp
|
||||
type CoverArtImageProps = BaseImageProps & CoverArtProp
|
||||
const Image: React.FC<{ file?: DownloadFile } & BaseProps> = ({ file, style, imageStyle, resizeMode }) => {
|
||||
const [source, setSource] = useState<number | { uri: string }>(
|
||||
file && file.progress === 1 ? { uri: `file://${file.path}` } : require('@res/fallback.png'),
|
||||
)
|
||||
|
||||
type CoverArtProps = BaseProps & CoverArtProp & Partial<ArtistIdProp>
|
||||
|
||||
const ArtistImageFallback: React.FC<{
|
||||
enableLoading: () => void
|
||||
}> = ({ enableLoading }) => {
|
||||
useEffect(() => {
|
||||
enableLoading()
|
||||
}, [enableLoading])
|
||||
return <></>
|
||||
if (file && file.progress === 1) {
|
||||
setSource({ uri: `file://${file.path}` })
|
||||
}
|
||||
}, [file])
|
||||
|
||||
return (
|
||||
<>
|
||||
<FastImage
|
||||
source={source}
|
||||
resizeMode={resizeMode || FastImage.resizeMode.contain}
|
||||
style={[{ height: style?.height, width: style?.width }, imageStyle]}
|
||||
onError={() => {
|
||||
setSource(require('@res/fallback.png'))
|
||||
}}
|
||||
/>
|
||||
<ActivityIndicator
|
||||
animating={file && file.progress < 1}
|
||||
size="large"
|
||||
color={colors.accent}
|
||||
style={styles.indicator}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const ArtistImage = React.memo<ArtistIdImageProps>(
|
||||
({ artistId, imageSize, style, imageStyle, resizeMode, enableLoading, disableLoading, fallbackError }) => {
|
||||
const artistInfo = useArtistInfo(artistId)
|
||||
const ArtistImage = React.memo<ArtistCoverArtProps>(props => {
|
||||
const file = useArtistCoverArtFile(props.artistId)
|
||||
|
||||
if (!artistInfo) {
|
||||
return <ArtistImageFallback enableLoading={enableLoading} />
|
||||
}
|
||||
return <Image file={file} {...props} />
|
||||
})
|
||||
|
||||
const uri = imageSize === 'thumbnail' ? artistInfo?.smallImageUrl : artistInfo?.largeImageUrl
|
||||
const CoverArtImage = React.memo<CoverArtProps>(props => {
|
||||
const file = useCoverArtFile(props.coverArt)
|
||||
|
||||
return (
|
||||
<FastImage
|
||||
source={{ uri }}
|
||||
style={[{ height: style?.height, width: style?.width }, imageStyle]}
|
||||
resizeMode={resizeMode || FastImage.resizeMode.contain}
|
||||
onProgress={enableLoading}
|
||||
onLoadEnd={disableLoading}
|
||||
onError={fallbackError}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
return <Image file={file} {...props} />
|
||||
})
|
||||
|
||||
const CoverArtImage = React.memo<CoverArtImageProps>(
|
||||
({ coverArt, imageSize, style, imageStyle, resizeMode, enableLoading, disableLoading, fallbackError }) => {
|
||||
const coverArtUri = useCoverArtUri()
|
||||
|
||||
return (
|
||||
<FastImage
|
||||
source={{ uri: coverArtUri(coverArt, imageSize) }}
|
||||
style={[{ height: style?.height, width: style?.width }, imageStyle]}
|
||||
resizeMode={resizeMode || FastImage.resizeMode.contain}
|
||||
onProgress={enableLoading}
|
||||
onLoadEnd={disableLoading}
|
||||
onError={fallbackError}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
const CoverArt: React.FC<CoverArtProps> = ({ coverArt, artistId, resizeMode, imageSize, style, imageStyle, round }) => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [fallback, setFallback] = useState(false)
|
||||
|
||||
const enableLoading = React.useCallback(() => setLoading(true), [])
|
||||
const disableLoading = React.useCallback(() => setLoading(false), [])
|
||||
const fallbackError = React.useCallback(() => {
|
||||
setFallback(true)
|
||||
setLoading(false)
|
||||
}, [])
|
||||
|
||||
imageSize = imageSize === undefined ? 'thumbnail' : 'original'
|
||||
round = round === undefined ? artistId !== undefined : round
|
||||
|
||||
const viewStyles = [style]
|
||||
if (round) {
|
||||
const CoverArt: React.FC<CoverArtProps | ArtistCoverArtProps> = props => {
|
||||
const viewStyles = [props.style]
|
||||
if (props.round) {
|
||||
viewStyles.push(styles.round)
|
||||
}
|
||||
|
||||
let ImageComponent
|
||||
if (artistId) {
|
||||
ImageComponent = (
|
||||
<ArtistImage
|
||||
artistId={artistId}
|
||||
imageSize={imageSize}
|
||||
style={style}
|
||||
imageStyle={imageStyle}
|
||||
resizeMode={resizeMode}
|
||||
enableLoading={enableLoading}
|
||||
disableLoading={disableLoading}
|
||||
fallbackError={fallbackError}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
ImageComponent = (
|
||||
<CoverArtImage
|
||||
coverArt={coverArt}
|
||||
imageSize={imageSize}
|
||||
style={style}
|
||||
imageStyle={imageStyle}
|
||||
resizeMode={resizeMode}
|
||||
enableLoading={enableLoading}
|
||||
disableLoading={disableLoading}
|
||||
fallbackError={fallbackError}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const coverArtImage = useCallback(() => <CoverArtImage {...(props as CoverArtProps)} />, [props])
|
||||
const artistImage = useCallback(() => <ArtistImage {...(props as ArtistCoverArtProps)} />, [props])
|
||||
|
||||
if (fallback) {
|
||||
ImageComponent = (
|
||||
<FastImage
|
||||
source={require('@res/fallback.png')}
|
||||
style={[{ height: style?.height, width: style?.width }, imageStyle]}
|
||||
/>
|
||||
)
|
||||
let ImageComponent
|
||||
switch (props.type) {
|
||||
case 'artist':
|
||||
ImageComponent = artistImage
|
||||
break
|
||||
default:
|
||||
ImageComponent = coverArtImage
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={viewStyles}>
|
||||
{ImageComponent}
|
||||
<ActivityIndicator animating={loading} size="large" color={colors.accent} style={styles.indicator} />
|
||||
<ImageComponent />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { ViewStyle } from 'react-native'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import ImageColors from 'react-native-image-colors'
|
||||
import { AndroidImageColors } from 'react-native-image-colors/lib/typescript/types'
|
||||
import colors from '@app/styles/colors'
|
||||
@@ -12,28 +11,27 @@ const ImageGradientBackground: React.FC<{
|
||||
width?: number | string
|
||||
position?: 'relative' | 'absolute'
|
||||
style?: ViewStyle
|
||||
imageUri?: string
|
||||
imageKey?: string
|
||||
}> = ({ height, width, position, style, imageUri, imageKey, children }) => {
|
||||
imagePath?: string
|
||||
}> = ({ height, width, position, style, imagePath, children }) => {
|
||||
const [highColor, setHighColor] = useState<string>(colors.gradient.high)
|
||||
const navigation = useNavigation()
|
||||
|
||||
useEffect(() => {
|
||||
async function getColors() {
|
||||
if (imageUri === undefined) {
|
||||
if (imagePath === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
const cachedResult = ImageColors.cache.getItem(imageKey ? imageKey : imageUri)
|
||||
const cachedResult = ImageColors.cache.getItem(imagePath)
|
||||
|
||||
let res: AndroidImageColors
|
||||
if (cachedResult) {
|
||||
res = cachedResult as AndroidImageColors
|
||||
} else {
|
||||
const path = await FastImage.getCachePath({ uri: imageUri })
|
||||
res = (await ImageColors.getColors(path ? `file://${path}` : imageUri, {
|
||||
const path = `file://${imagePath}`
|
||||
res = (await ImageColors.getColors(path, {
|
||||
cache: true,
|
||||
key: imageKey ? imageKey : imageUri,
|
||||
key: imagePath,
|
||||
})) as AndroidImageColors
|
||||
}
|
||||
|
||||
@@ -44,7 +42,7 @@ const ImageGradientBackground: React.FC<{
|
||||
}
|
||||
}
|
||||
getColors()
|
||||
}, [imageUri, imageKey])
|
||||
}, [imagePath])
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
|
||||
@@ -4,7 +4,7 @@ import dimensions from '@app/styles/dimensions'
|
||||
import React from 'react'
|
||||
import { ScrollView, ScrollViewProps, useWindowDimensions } from 'react-native'
|
||||
|
||||
const ImageGradientScrollView: React.FC<ScrollViewProps & { imageUri?: string; imageKey?: string }> = props => {
|
||||
const ImageGradientScrollView: React.FC<ScrollViewProps & { imagePath?: string }> = props => {
|
||||
const layout = useWindowDimensions()
|
||||
|
||||
const minHeight = layout.height - (dimensions.top() + dimensions.bottom())
|
||||
@@ -20,7 +20,7 @@ const ImageGradientScrollView: React.FC<ScrollViewProps & { imageUri?: string; i
|
||||
},
|
||||
]}
|
||||
contentContainerStyle={[{ minHeight }, props.contentContainerStyle]}>
|
||||
<ImageGradientBackground height={minHeight} imageUri={props.imageUri} imageKey={props.imageKey} />
|
||||
<ImageGradientBackground height={minHeight} imagePath={props.imagePath} />
|
||||
{props.children}
|
||||
</ScrollView>
|
||||
)
|
||||
|
||||
@@ -65,7 +65,6 @@ const ListItem: React.FC<{
|
||||
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
|
||||
|
||||
if (!onPress) {
|
||||
@@ -148,18 +147,19 @@ const ListItem: React.FC<{
|
||||
title = <TitleText title={item.name} />
|
||||
}
|
||||
|
||||
const artStyle = { ...styles.art, ...sizeStyle.art }
|
||||
const resizeMode = FastImage.resizeMode.cover
|
||||
let coverArt = <></>
|
||||
if (item.itemType === 'artist') {
|
||||
coverArt = <CoverArt type="artist" artistId={item.id} round={true} style={artStyle} resizeMode={resizeMode} />
|
||||
} else {
|
||||
coverArt = <CoverArt type="cover" coverArt={item.coverArt} style={artStyle} resizeMode={resizeMode} />
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, sizeStyle.container]}>
|
||||
<PressableComponent>
|
||||
{showArt ? (
|
||||
<CoverArt
|
||||
{...artSource}
|
||||
style={{ ...styles.art, ...sizeStyle.art }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{showArt ? coverArt : <></>}
|
||||
<View style={styles.text}>
|
||||
{title}
|
||||
{subtitle ? (
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import { State } from 'react-native-track-player'
|
||||
import IconFA5 from 'react-native-vector-icons/FontAwesome5'
|
||||
|
||||
const ProgressBar = () => {
|
||||
const ProgressBar = React.memo(() => {
|
||||
const { position, duration } = useStore(selectTrackPlayer.progress)
|
||||
|
||||
let progress = 0
|
||||
@@ -25,7 +25,7 @@ const ProgressBar = () => {
|
||||
<View style={[progressStyles.right, { flex: 1 - progress }]} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const progressStyles = StyleSheet.create({
|
||||
container: {
|
||||
@@ -40,9 +40,7 @@ const progressStyles = StyleSheet.create({
|
||||
},
|
||||
})
|
||||
|
||||
const NowPlayingBar = () => {
|
||||
const navigation = useNavigation()
|
||||
const track = useStore(selectTrackPlayer.currentTrack)
|
||||
const Controls = React.memo(() => {
|
||||
const playerState = useStore(selectTrackPlayer.playerState)
|
||||
const play = usePlay()
|
||||
const pause = usePause()
|
||||
@@ -61,6 +59,19 @@ const NowPlayingBar = () => {
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.controls}>
|
||||
<PressableOpacity onPress={playPauseAction} hitSlop={14}>
|
||||
<IconFA5 name={playPauseIcon} size={28} color="white" />
|
||||
</PressableOpacity>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
const NowPlayingBar = React.memo(() => {
|
||||
const navigation = useNavigation()
|
||||
const track = useStore(selectTrackPlayer.currentTrack)
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => navigation.navigate('now-playing')}
|
||||
@@ -68,8 +79,9 @@ const NowPlayingBar = () => {
|
||||
<ProgressBar />
|
||||
<View style={styles.subContainer}>
|
||||
<CoverArt
|
||||
type="cover"
|
||||
style={{ height: styles.subContainer.height, width: styles.subContainer.height }}
|
||||
coverArt={track?.coverArt || '-1'}
|
||||
coverArt={track?.coverArt}
|
||||
/>
|
||||
<View style={styles.detailsContainer}>
|
||||
<Text numberOfLines={1} style={styles.detailsTitle}>
|
||||
@@ -79,15 +91,11 @@ const NowPlayingBar = () => {
|
||||
{track?.artist}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.controls}>
|
||||
<PressableOpacity onPress={playPauseAction} hitSlop={14}>
|
||||
<IconFA5 name={playPauseIcon} size={28} color="white" />
|
||||
</PressableOpacity>
|
||||
</View>
|
||||
<Controls />
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
||||
Reference in New Issue
Block a user