reorg again, absolute (module) imports

This commit is contained in:
austinried
2021-07-08 12:21:44 +09:00
parent a94a011a18
commit ea4421b7af
54 changed files with 186 additions and 251 deletions

View File

@@ -0,0 +1,57 @@
import { useAtomValue } from 'jotai/utils'
import React from 'react'
import { ActivityIndicator, View } from 'react-native'
import FastImage from 'react-native-fast-image'
import LinearGradient from 'react-native-linear-gradient'
import { albumArtAtomFamily } from '@app/state/music'
import colors from '@app/styles/colors'
import CoverArt from '@app/components/CoverArt'
interface AlbumArtProps {
id: string
height: number
width: number
}
const AlbumArt: React.FC<AlbumArtProps> = ({ id, height, width }) => {
const albumArt = useAtomValue(albumArtAtomFamily(id))
const Placeholder = () => (
<LinearGradient colors={[colors.accent, colors.accentLow]}>
<FastImage
source={require('../../res/record.png')}
style={{ height, width }}
resizeMode={FastImage.resizeMode.contain}
/>
</LinearGradient>
)
return (
<CoverArt
PlaceholderComponent={Placeholder}
height={height}
width={width}
coverArtUri={width > 128 ? albumArt?.uri : albumArt?.thumbUri}
/>
)
}
const AlbumArtFallback: React.FC<AlbumArtProps> = ({ height, width }) => (
<View
style={{
height,
width,
alignItems: 'center',
justifyContent: 'center',
}}>
<ActivityIndicator size="small" color={colors.accent} />
</View>
)
const AlbumArtLoader: React.FC<AlbumArtProps> = props => (
<React.Suspense fallback={<AlbumArtFallback {...props} />}>
<AlbumArt {...props} />
</React.Suspense>
)
export default React.memo(AlbumArtLoader)

View File

@@ -0,0 +1,201 @@
import { useAtomValue } from 'jotai/utils'
import React from 'react'
import { ActivityIndicator, View } from 'react-native'
import FastImage from 'react-native-fast-image'
import LinearGradient from 'react-native-linear-gradient'
import { artistArtAtomFamily } from '@app/state/music'
import colors from '@app/styles/colors'
import CoverArt from '@app/components/CoverArt'
interface ArtistArtSizeProps {
height: number
width: number
}
interface ArtistArtXUpProps extends ArtistArtSizeProps {
coverArtUris: string[]
}
interface ArtistArtProps extends ArtistArtSizeProps {
id: string
}
const PlaceholderContainer: React.FC<ArtistArtSizeProps> = ({ height, width, children }) => (
<LinearGradient
colors={[colors.accent, colors.accentLow]}
style={{
height,
width,
alignItems: 'center',
justifyContent: 'center',
}}>
{children}
</LinearGradient>
)
const FourUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
const halfHeight = height / 2
const halfWidth = width / 2
return (
<PlaceholderContainer height={height} width={width}>
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
<FastImage
source={{ uri: coverArtUris[0] }}
style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover}
/>
<FastImage
source={{ uri: coverArtUris[1] }}
style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover}
/>
</View>
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
<FastImage
source={{ uri: coverArtUris[2] }}
style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover}
/>
<FastImage
source={{ uri: coverArtUris[3] }}
style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover}
/>
</View>
</PlaceholderContainer>
)
}
const ThreeUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
const halfHeight = height / 2
const halfWidth = width / 2
return (
<PlaceholderContainer height={height} width={width}>
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
<FastImage
source={{ uri: coverArtUris[0] }}
style={{ height: halfHeight, width }}
resizeMode={FastImage.resizeMode.cover}
/>
</View>
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
<FastImage
source={{ uri: coverArtUris[1] }}
style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover}
/>
<FastImage
source={{ uri: coverArtUris[2] }}
style={{ height: halfHeight, width: halfWidth }}
resizeMode={FastImage.resizeMode.cover}
/>
</View>
</PlaceholderContainer>
)
}
const TwoUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
const halfHeight = height / 2
return (
<PlaceholderContainer height={height} width={width}>
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
<FastImage
source={{ uri: coverArtUris[0] }}
style={{ height: halfHeight, width }}
resizeMode={FastImage.resizeMode.cover}
/>
</View>
<View style={{ width, height: halfHeight, flexDirection: 'row' }}>
<FastImage
source={{ uri: coverArtUris[1] }}
style={{ height: halfHeight, width }}
resizeMode={FastImage.resizeMode.cover}
/>
</View>
</PlaceholderContainer>
)
}
const OneUp: React.FC<ArtistArtXUpProps> = ({ height, width, coverArtUris }) => {
return (
<PlaceholderContainer height={height} width={width}>
<FastImage source={{ uri: coverArtUris[0] }} style={{ height, width }} resizeMode={FastImage.resizeMode.cover} />
</PlaceholderContainer>
)
}
const NoneUp: React.FC<ArtistArtSizeProps> = ({ height, width }) => {
return (
<PlaceholderContainer height={height} width={width}>
<FastImage
source={require('../../res/mic_on-fill.png')}
style={{
height: height - height / 4,
width: width - width / 4,
}}
resizeMode={FastImage.resizeMode.cover}
/>
</PlaceholderContainer>
)
}
const ArtistArt: React.FC<ArtistArtProps> = ({ id, height, width }) => {
const artistArt = useAtomValue(artistArtAtomFamily(id))
const Placeholder = () => {
const none = <NoneUp height={height} width={width} />
if (!artistArt || !artistArt.coverArtUris) {
return none
}
const { coverArtUris } = artistArt
if (coverArtUris.length >= 4) {
return <FourUp height={height} width={width} coverArtUris={coverArtUris} />
}
if (coverArtUris.length === 3) {
return <ThreeUp height={height} width={width} coverArtUris={coverArtUris} />
}
if (coverArtUris.length === 2) {
return <TwoUp height={height} width={width} coverArtUris={coverArtUris} />
}
if (coverArtUris.length === 1) {
return <OneUp height={height} width={width} coverArtUris={coverArtUris} />
}
return none
}
return (
<View
style={{
borderRadius: height / 2,
overflow: 'hidden',
}}>
<CoverArt PlaceholderComponent={Placeholder} height={height} width={width} coverArtUri={artistArt?.uri} />
</View>
)
}
const ArtistArtFallback: React.FC<ArtistArtProps> = ({ height, width }) => (
<View
style={{
height,
width,
alignItems: 'center',
justifyContent: 'center',
}}>
<ActivityIndicator size="small" color={colors.accent} />
</View>
)
const ArtistArtLoader: React.FC<ArtistArtProps> = props => (
<React.Suspense fallback={<ArtistArtFallback {...props} />}>
<ArtistArt {...props} />
</React.Suspense>
)
export default React.memo(ArtistArtLoader)

31
app/components/Button.tsx Normal file
View File

@@ -0,0 +1,31 @@
import React, { useState } from 'react'
import { GestureResponderEvent, Pressable, Text } from 'react-native'
import colors from '@app/styles/colors'
import text from '@app/styles/text'
const Button: React.FC<{
title: string
onPress: (event: GestureResponderEvent) => void
}> = ({ title, onPress }) => {
const [opacity, setOpacity] = useState(1)
return (
<Pressable
onPress={onPress}
onPressIn={() => setOpacity(0.6)}
onPressOut={() => setOpacity(1)}
onLongPress={() => setOpacity(1)}
style={{
backgroundColor: colors.accent,
paddingHorizontal: 24,
minHeight: 42,
justifyContent: 'center',
borderRadius: 1000,
opacity,
}}>
<Text style={{ ...text.button }}>{title}</Text>
</Pressable>
)
}
export default Button

View File

@@ -0,0 +1,63 @@
import React, { useEffect, useState } from 'react'
import { ActivityIndicator, StyleSheet, View } from 'react-native'
import FastImage from 'react-native-fast-image'
import colors from '@app/styles/colors'
const CoverArt: React.FC<{
PlaceholderComponent: () => JSX.Element
height?: string | number
width?: string | number
coverArtUri?: string
}> = ({ PlaceholderComponent, height, width, coverArtUri }) => {
const [placeholderVisible, setPlaceholderVisible] = useState(false)
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!coverArtUri) {
setLoading(false)
}
}, [coverArtUri, setLoading])
const Image = () => (
<FastImage
source={{ uri: coverArtUri, priority: 'high' }}
style={{ ...styles.image, opacity: placeholderVisible ? 0 : 1 }}
resizeMode={FastImage.resizeMode.contain}
onError={() => {
setLoading(false)
setPlaceholderVisible(true)
}}
onLoadEnd={() => setLoading(false)}
/>
)
return (
<View style={{ ...styles.container, height, width }}>
{coverArtUri ? <Image /> : <></>}
<View style={{ ...styles.placeholderContainer, opacity: placeholderVisible ? 1 : 0 }}>
<PlaceholderComponent />
</View>
<ActivityIndicator style={styles.indicator} animating={loading} size={'large'} color={colors.accent} />
</View>
)
}
const styles = StyleSheet.create({
container: {},
image: {
height: '100%',
width: '100%',
},
placeholderContainer: {
height: '100%',
width: '100%',
position: 'absolute',
},
indicator: {
height: '100%',
width: '100%',
position: 'absolute',
},
})
export default React.memo(CoverArt)

View File

@@ -0,0 +1,31 @@
import React from 'react'
import { useWindowDimensions, ViewStyle } from 'react-native'
import LinearGradient from 'react-native-linear-gradient'
import colorStyles from '@app/styles/colors'
const GradientBackground: React.FC<{
height?: number | string
width?: number | string
position?: 'relative' | 'absolute'
style?: ViewStyle
colors?: string[]
locations?: number[]
}> = ({ height, width, position, style, colors, locations, children }) => {
const layout = useWindowDimensions()
return (
<LinearGradient
colors={colors || [colorStyles.gradient.high, colorStyles.gradient.low]}
locations={locations || [0.01, 0.7]}
style={{
...style,
width: width || '100%',
height: height || layout.height,
position: position || 'absolute',
}}>
{children}
</LinearGradient>
)
}
export default GradientBackground

View File

@@ -0,0 +1,24 @@
import React from 'react'
import { FlatList, FlatListProps, useWindowDimensions } from 'react-native'
import colors from '@app/styles/colors'
import GradientBackground from '@app/components/GradientBackground'
function GradientFlatList<ItemT>(props: FlatListProps<ItemT>) {
const layout = useWindowDimensions()
return (
<FlatList
{...props}
style={{
...(props.style as any),
backgroundColor: colors.gradient.low,
}}
ListHeaderComponent={() => <GradientBackground position="relative" />}
ListHeaderComponentStyle={{
marginBottom: -layout.height,
}}
/>
)
}
export default GradientFlatList

View File

@@ -0,0 +1,18 @@
import React from 'react'
import { ScrollView, ScrollViewProps, ViewStyle } from 'react-native'
import colors from '@app/styles/colors'
import GradientBackground from '@app/components/GradientBackground'
const GradientScrollView: React.FC<ScrollViewProps> = props => {
props.style = props.style || {}
;(props.style as ViewStyle).backgroundColor = colors.gradient.low
return (
<ScrollView overScrollMode="never" {...props}>
<GradientBackground />
{props.children}
</ScrollView>
)
}
export default GradientScrollView

View File

@@ -0,0 +1,70 @@
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'
import GradientBackground from '@app/components/GradientBackground'
const ImageGradientBackground: React.FC<{
height?: number | string
width?: number | string
position?: 'relative' | 'absolute'
style?: ViewStyle
imageUri?: string
imageKey?: string
}> = ({ height, width, position, style, imageUri, imageKey, children }) => {
const [highColor, setHighColor] = useState<string>(colors.gradient.high)
const navigation = useNavigation()
useEffect(() => {
async function getColors() {
if (imageUri === undefined) {
return
}
const cachedResult = ImageColors.cache.getItem(imageKey ? imageKey : imageUri)
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, {
cache: true,
key: imageKey ? imageKey : imageUri,
})) as AndroidImageColors
}
if (res.muted && res.muted !== '#000000') {
setHighColor(res.muted)
} else if (res.darkMuted && res.darkMuted !== '#000000') {
setHighColor(res.darkMuted)
}
}
getColors()
}, [imageUri, imageKey])
useEffect(() => {
navigation.setOptions({
headerStyle: {
backgroundColor: highColor,
},
})
}, [navigation, highColor])
return (
<GradientBackground
height={height}
width={width}
position={position}
style={style}
colors={[highColor, colors.gradient.low]}
locations={[0.1, 1.0]}>
{children}
</GradientBackground>
)
}
export default ImageGradientBackground

View File

@@ -0,0 +1,30 @@
import React, { useState } from 'react'
import { LayoutRectangle, ScrollView, ScrollViewProps } from 'react-native'
import colors from '@app/styles/colors'
import ImageGradientBackground from '@app/components/ImageGradientBackground'
const ImageGradientScrollView: React.FC<ScrollViewProps & { imageUri?: string; imageKey?: string }> = props => {
const [layout, setLayout] = useState<LayoutRectangle | undefined>(undefined)
props.style = props.style || {}
if (typeof props.style === 'object' && props.style !== null) {
props.style = {
...props.style,
backgroundColor: colors.gradient.low,
}
}
return (
<ScrollView
overScrollMode="never"
{...props}
onLayout={event => {
setLayout(event.nativeEvent.layout)
}}>
<ImageGradientBackground height={layout?.height} imageUri={props.imageUri} imageKey={props.imageKey} />
{props.children}
</ScrollView>
)
}
export default ImageGradientScrollView

View File

@@ -0,0 +1,132 @@
import React from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { useNavigation } from '@react-navigation/native'
import { useAtomValue } from 'jotai/utils'
import { currentTrackAtom, playerStateAtom, usePause, usePlay, useProgress } from '@app/state/trackplayer'
import CoverArt from '@app/components/CoverArt'
import colors from '@app/styles/colors'
import { Font } from '@app/styles/text'
import { State } from 'react-native-track-player'
import PressableOpacity from '@app/components/PressableOpacity'
import IconFA5 from 'react-native-vector-icons/FontAwesome5'
const ProgressBar = () => {
const { position, duration } = useProgress()
let progress = 0
if (duration > 0) {
progress = position / duration
}
return (
<View style={progressStyles.container}>
<View style={{ ...progressStyles.left, flex: progress }} />
<View style={{ ...progressStyles.right, flex: 1 - progress }} />
</View>
)
}
const progressStyles = StyleSheet.create({
container: {
height: 2,
flexDirection: 'row',
},
left: {
backgroundColor: colors.text.primary,
},
right: {
backgroundColor: '#595959',
},
})
const NowPlayingBar = () => {
const navigation = useNavigation()
const track = useAtomValue(currentTrackAtom)
const playerState = useAtomValue(playerStateAtom)
const play = usePlay()
const pause = usePause()
let playPauseIcon: string
let playPauseAction: () => void
switch (playerState) {
case State.Playing:
case State.Buffering:
case State.Connecting:
playPauseIcon = 'pause'
playPauseAction = pause
break
default:
playPauseIcon = 'play'
playPauseAction = play
break
}
return (
<Pressable
onPress={() => navigation.navigate('NowPlaying')}
style={{ ...styles.container, display: track ? 'flex' : 'none' }}>
<ProgressBar />
<View style={styles.subContainer}>
<CoverArt
PlaceholderComponent={() => <Text>hi</Text>}
height={styles.subContainer.height}
width={styles.subContainer.height}
coverArtUri={track?.artworkThumb}
/>
<View style={styles.detailsContainer}>
<Text numberOfLines={1} style={styles.detailsTitle}>
{track?.title}
</Text>
<Text numberOfLines={1} style={styles.detailsAlbum}>
{track?.artist}
</Text>
</View>
<View style={styles.controls}>
<PressableOpacity onPress={playPauseAction} hitSlop={14}>
<IconFA5 name={playPauseIcon} size={28} color="white" />
</PressableOpacity>
</View>
</View>
</Pressable>
)
}
const styles = StyleSheet.create({
container: {
width: '100%',
backgroundColor: colors.gradient.high,
borderBottomColor: colors.gradient.low,
borderBottomWidth: 1,
},
subContainer: {
height: 60,
flexDirection: 'row',
},
detailsContainer: {
flex: 1,
height: '100%',
justifyContent: 'center',
marginLeft: 10,
},
detailsTitle: {
fontFamily: Font.semiBold,
fontSize: 13,
color: colors.text.primary,
},
detailsAlbum: {
fontFamily: Font.regular,
fontSize: 13,
color: colors.text.secondary,
},
controls: {
flexDirection: 'row',
height: '100%',
justifyContent: 'center',
alignItems: 'center',
marginRight: 18,
marginLeft: 12,
},
})
export default NowPlayingBar

View File

@@ -0,0 +1,44 @@
import React, { useEffect, useState } from 'react'
import { LayoutRectangle, Pressable, PressableProps } from 'react-native'
type PressableOpacityProps = PressableProps & {
ripple?: boolean
}
const PressableOpacity: React.FC<PressableOpacityProps> = props => {
const [opacity, setOpacity] = useState(1)
const [dimensions, setDimensions] = useState<LayoutRectangle | undefined>(undefined)
useEffect(() => {
props.disabled === true ? setOpacity(0.3) : setOpacity(1)
}, [props.disabled])
return (
<Pressable
{...props}
style={[{ justifyContent: 'center', alignItems: 'center' }, props.style as any, { opacity }]}
android_ripple={
props.ripple
? {
color: 'rgba(255,255,255,0.26)',
radius: dimensions ? dimensions.width / 2 : undefined,
}
: undefined
}
onLayout={event => setDimensions(event.nativeEvent.layout)}
onPressIn={() => {
if (!props.disabled) {
setOpacity(0.4)
}
}}
onPressOut={() => {
if (!props.disabled) {
setOpacity(1)
}
}}>
{props.children}
</Pressable>
)
}
export default React.memo(PressableOpacity)

View File

@@ -0,0 +1,157 @@
import { useAppState } from '@react-native-community/hooks'
import { useAtomValue, useUpdateAtom } from 'jotai/utils'
import React, { useEffect } from 'react'
import { View } from 'react-native'
import { Event, State, useProgress, useTrackPlayerEvents } from 'react-native-track-player'
import {
currentTrackAtom,
playerStateAtom,
progressAtom,
progressSubsAtom,
queueWriteAtom,
useRefreshCurrentTrack,
useRefreshPlayerState,
useRefreshProgress,
useRefreshQueue,
} from '@app/state/trackplayer'
const AppActiveResponder: React.FC<{
update: () => void
}> = ({ update }) => {
const appState = useAppState()
useEffect(() => {
if (appState === 'active') {
update()
}
}, [appState, update])
return <></>
}
type Payload = { type: Event; [key: string]: any }
const TrackPlayerEventResponder: React.FC<{
update: (payload?: Payload) => void
events: Event[]
}> = ({ update, events }) => {
useTrackPlayerEvents(events, update)
return <AppActiveResponder update={update} />
}
const CurrentTrackState = () => {
const setCurrentTrack = useUpdateAtom(currentTrackAtom)
const refreshCurrentTrack = useRefreshCurrentTrack()
const update = async (payload?: Payload) => {
const queueEnded = payload?.type === Event.PlaybackQueueEnded && 'track' in payload
const remoteStop = payload?.type === Event.RemoteStop
if (queueEnded || remoteStop) {
setCurrentTrack(undefined)
return
}
await refreshCurrentTrack()
}
return (
<TrackPlayerEventResponder
events={[
Event.PlaybackQueueEnded,
Event.PlaybackMetadataReceived,
Event.RemoteDuck,
Event.RemoteNext,
Event.RemotePrevious,
Event.RemoteStop,
]}
update={update}
/>
)
}
const PlayerState = () => {
const setPlayerState = useUpdateAtom(playerStateAtom)
const refreshPlayerState = useRefreshPlayerState()
const update = async (payload?: Payload) => {
if (payload?.type === Event.RemoteStop) {
setPlayerState(State.None)
return
}
await refreshPlayerState()
}
return <TrackPlayerEventResponder events={[Event.PlaybackState, Event.RemoteStop]} update={update} />
}
const QueueState = () => {
const setQueue = useUpdateAtom(queueWriteAtom)
const refreshQueue = useRefreshQueue()
const update = async (payload?: Payload) => {
if (payload) {
setQueue([])
return
}
await refreshQueue()
}
return <TrackPlayerEventResponder events={[Event.RemoteStop]} update={update} />
}
const ProgressHook = () => {
const setProgress = useUpdateAtom(progressAtom)
const progress = useProgress(250)
useEffect(() => {
setProgress(progress)
}, [setProgress, progress])
return <></>
}
const ProgressState = () => {
const setProgress = useUpdateAtom(progressAtom)
const refreshProgress = useRefreshProgress()
const progressSubs = useAtomValue(progressSubsAtom)
const update = async (payload?: Payload) => {
if (payload) {
setProgress({ position: 0, duration: 0, buffered: 0 })
return
}
await refreshProgress()
}
if (progressSubs > 0) {
return (
<>
<ProgressHook />
<TrackPlayerEventResponder events={[Event.RemoteStop, Event.PlaybackTrackChanged]} update={update} />
</>
)
}
return <TrackPlayerEventResponder events={[Event.RemoteStop]} update={update} />
}
const Debug = () => {
const value = useAtomValue(currentTrackAtom)
useEffect(() => {
// ToastAndroid.show(value?.title || 'undefined', 1)
}, [value])
return <></>
}
const TrackPlayerState = () => (
<View>
<CurrentTrackState />
<PlayerState />
<QueueState />
<ProgressState />
<Debug />
</View>
)
export default TrackPlayerState