full reworked images to download (cache) first

This commit is contained in:
austinried
2021-08-13 11:42:25 +09:00
parent 187cce16d9
commit f82a9b55bd
17 changed files with 426 additions and 209 deletions

View File

@@ -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>
)
}

View File

@@ -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({

View File

@@ -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>
)

View File

@@ -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 ? (

View File

@@ -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: {