mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 15:02:42 +01:00
reorg again, absolute (module) imports
This commit is contained in:
194
app/screens/AlbumView.tsx
Normal file
194
app/screens/AlbumView.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import React, { useEffect } from 'react'
|
||||
import { ActivityIndicator, GestureResponderEvent, StyleSheet, Text, useWindowDimensions, View } from 'react-native'
|
||||
import IconFA from 'react-native-vector-icons/FontAwesome'
|
||||
import IconMat from 'react-native-vector-icons/MaterialIcons'
|
||||
import { albumAtomFamily } from '@app/state/music'
|
||||
import { currentTrackAtom, useSetQueue } from '@app/state/trackplayer'
|
||||
import colors from '@app/styles/colors'
|
||||
import text, { Font } from '@app/styles/text'
|
||||
import AlbumArt from '@app/components/AlbumArt'
|
||||
import Button from '@app/components/Button'
|
||||
import GradientBackground from '@app/components/GradientBackground'
|
||||
import ImageGradientScrollView from '@app/components/ImageGradientScrollView'
|
||||
import PressableOpacity from '@app/components/PressableOpacity'
|
||||
|
||||
const SongItem: React.FC<{
|
||||
id: string
|
||||
title: string
|
||||
artist?: string
|
||||
track?: number
|
||||
onPress: (event: GestureResponderEvent) => void
|
||||
}> = ({ id, title, artist, onPress }) => {
|
||||
const currentTrack = useAtomValue(currentTrackAtom)
|
||||
|
||||
return (
|
||||
<View style={songStyles.container}>
|
||||
<PressableOpacity onPress={onPress} style={songStyles.text}>
|
||||
<Text style={{ ...songStyles.title, color: currentTrack?.id === id ? colors.accent : colors.text.primary }}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text style={songStyles.subtitle}>{artist}</Text>
|
||||
</PressableOpacity>
|
||||
<View style={songStyles.controls}>
|
||||
<PressableOpacity onPress={undefined}>
|
||||
<IconFA name="star-o" size={26} color={colors.text.primary} />
|
||||
</PressableOpacity>
|
||||
<PressableOpacity onPress={undefined} style={songStyles.more}>
|
||||
<IconMat name="more-vert" size={32} color="white" />
|
||||
</PressableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const songStyles = StyleSheet.create({
|
||||
container: {
|
||||
marginTop: 20,
|
||||
marginLeft: 10,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
flex: 1,
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontFamily: Font.semiBold,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
fontFamily: Font.regular,
|
||||
color: colors.text.secondary,
|
||||
},
|
||||
controls: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginLeft: 10,
|
||||
},
|
||||
more: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
})
|
||||
|
||||
const AlbumDetails: React.FC<{
|
||||
id: string
|
||||
}> = ({ id }) => {
|
||||
const album = useAtomValue(albumAtomFamily(id))
|
||||
const layout = useWindowDimensions()
|
||||
const setQueue = useSetQueue()
|
||||
|
||||
const coverSize = layout.width - layout.width / 2.5
|
||||
|
||||
if (!album) {
|
||||
return <Text style={text.paragraph}>No Album</Text>
|
||||
}
|
||||
|
||||
return (
|
||||
<ImageGradientScrollView
|
||||
imageUri={album.coverArtThumbUri}
|
||||
imageKey={`${album.name}${album.artist}`}
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
alignItems: 'center',
|
||||
paddingTop: coverSize / 8,
|
||||
}}>
|
||||
<AlbumArt id={album.id} height={coverSize} width={coverSize} />
|
||||
<Text
|
||||
style={{
|
||||
...text.title,
|
||||
marginTop: 12,
|
||||
width: layout.width - layout.width / 8,
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
{album.name}
|
||||
</Text>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
...text.itemSubtitle,
|
||||
fontSize: 14,
|
||||
marginTop: 4,
|
||||
marginBottom: 20,
|
||||
width: layout.width - layout.width / 8,
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
{album.artist}
|
||||
{album.year ? ` • ${album.year}` : ''}
|
||||
</Text>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
}}>
|
||||
<Button title="Play Album" onPress={() => setQueue(album.songs, album.name, album.songs[0].id)} />
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
width: layout.width - layout.width / 20,
|
||||
marginTop: 20,
|
||||
marginBottom: 30,
|
||||
}}>
|
||||
{album.songs
|
||||
.sort((a, b) => {
|
||||
if (b.track && a.track) {
|
||||
return a.track - b.track
|
||||
} else {
|
||||
return a.title.localeCompare(b.title)
|
||||
}
|
||||
})
|
||||
.map(s => (
|
||||
<SongItem
|
||||
key={s.id}
|
||||
id={s.id}
|
||||
title={s.title}
|
||||
artist={s.artist}
|
||||
track={s.track}
|
||||
onPress={() => setQueue(album.songs, album.name, s.id)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</ImageGradientScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
const AlbumViewFallback = () => {
|
||||
const layout = useWindowDimensions()
|
||||
|
||||
const coverSize = layout.width - layout.width / 2.5
|
||||
|
||||
return (
|
||||
<GradientBackground
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
paddingTop: coverSize / 8 + coverSize / 2 - 18,
|
||||
}}>
|
||||
<ActivityIndicator size="large" color={colors.accent} />
|
||||
</GradientBackground>
|
||||
)
|
||||
}
|
||||
|
||||
const AlbumView: React.FC<{
|
||||
id: string
|
||||
title: string
|
||||
}> = ({ id, title }) => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({ title })
|
||||
})
|
||||
|
||||
return (
|
||||
<React.Suspense fallback={<AlbumViewFallback />}>
|
||||
<AlbumDetails id={id} />
|
||||
</React.Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(AlbumView)
|
||||
49
app/screens/ArtistView.tsx
Normal file
49
app/screens/ArtistView.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Text } from 'react-native'
|
||||
import { artistInfoAtomFamily } from '@app/state/music'
|
||||
import text from '@app/styles/text'
|
||||
import ArtistArt from '@app/components/ArtistArt'
|
||||
import GradientScrollView from '@app/components/GradientScrollView'
|
||||
|
||||
const ArtistDetails: React.FC<{ id: string }> = ({ id }) => {
|
||||
const artist = useAtomValue(artistInfoAtomFamily(id))
|
||||
|
||||
if (!artist) {
|
||||
return <></>
|
||||
}
|
||||
|
||||
return (
|
||||
<GradientScrollView
|
||||
style={{
|
||||
flex: 1,
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
alignItems: 'center',
|
||||
// paddingTop: coverSize / 8,
|
||||
}}>
|
||||
<Text style={text.paragraph}>{artist.name}</Text>
|
||||
<ArtistArt id={artist.id} height={200} width={200} />
|
||||
</GradientScrollView>
|
||||
)
|
||||
}
|
||||
|
||||
const ArtistView: React.FC<{
|
||||
id: string
|
||||
title: string
|
||||
}> = ({ id, title }) => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({ title })
|
||||
})
|
||||
|
||||
return (
|
||||
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||
<ArtistDetails id={id} />
|
||||
</React.Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(ArtistView)
|
||||
36
app/screens/ArtistsList.tsx
Normal file
36
app/screens/ArtistsList.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import { FlatList, Text, View } from 'react-native'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import { Artist } from '@app/models/music'
|
||||
import { artistsAtom } from '@app/state/music'
|
||||
|
||||
const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => (
|
||||
<View>
|
||||
<Text>{item.id}</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 60,
|
||||
paddingBottom: 400,
|
||||
}}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
|
||||
const List = () => {
|
||||
const artists = useAtomValue(artistsAtom)
|
||||
|
||||
const renderItem: React.FC<{ item: Artist }> = ({ item }) => <ArtistItem item={item} />
|
||||
|
||||
return <FlatList data={artists} renderItem={renderItem} keyExtractor={item => item.id} />
|
||||
}
|
||||
|
||||
const ArtistsList = () => (
|
||||
<View>
|
||||
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||
<List />
|
||||
</React.Suspense>
|
||||
</View>
|
||||
)
|
||||
|
||||
export default ArtistsList
|
||||
90
app/screens/LibraryAlbums.tsx
Normal file
90
app/screens/LibraryAlbums.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Pressable, Text, View } from 'react-native'
|
||||
import { Album } from '@app/models/music'
|
||||
import { albumsAtom, albumsUpdatingAtom, useUpdateAlbums } from '@app/state/music'
|
||||
import textStyles from '@app/styles/text'
|
||||
import AlbumArt from '@app/components/AlbumArt'
|
||||
import GradientFlatList from '@app/components/GradientFlatList'
|
||||
|
||||
const AlbumItem: React.FC<{
|
||||
id: string
|
||||
name: string
|
||||
artist?: string
|
||||
}> = ({ id, name, artist }) => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
const size = 125
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
marginVertical: 8,
|
||||
flex: 1 / 3,
|
||||
}}
|
||||
onPress={() => navigation.navigate('AlbumView', { id, title: name })}>
|
||||
<AlbumArt id={id} height={size} width={size} />
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
width: size,
|
||||
}}>
|
||||
<Text
|
||||
style={{
|
||||
...textStyles.itemTitle,
|
||||
marginTop: 4,
|
||||
}}
|
||||
numberOfLines={2}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text style={{ ...textStyles.itemSubtitle }} numberOfLines={1}>
|
||||
{artist}
|
||||
</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
const MemoAlbumItem = React.memo(AlbumItem)
|
||||
|
||||
const AlbumListRenderItem: React.FC<{ item: Album }> = ({ item }) => (
|
||||
<MemoAlbumItem id={item.id} name={item.name} artist={item.artist} />
|
||||
)
|
||||
|
||||
const AlbumsList = () => {
|
||||
const albums = useAtomValue(albumsAtom)
|
||||
const updating = useAtomValue(albumsUpdatingAtom)
|
||||
const updateAlbums = useUpdateAlbums()
|
||||
|
||||
const albumsList = Object.values(albums)
|
||||
|
||||
useEffect(() => {
|
||||
if (albumsList.length === 0) {
|
||||
updateAlbums()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<GradientFlatList
|
||||
data={albumsList}
|
||||
renderItem={AlbumListRenderItem}
|
||||
keyExtractor={item => item.id}
|
||||
numColumns={3}
|
||||
removeClippedSubviews={true}
|
||||
refreshing={updating}
|
||||
onRefresh={updateAlbums}
|
||||
overScrollMode="never"
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const AlbumsTab = () => (
|
||||
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||
<AlbumsList />
|
||||
</React.Suspense>
|
||||
)
|
||||
|
||||
export default React.memo(AlbumsTab)
|
||||
69
app/screens/LibraryArtists.tsx
Normal file
69
app/screens/LibraryArtists.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Pressable } from 'react-native'
|
||||
import { Text } from 'react-native'
|
||||
import { Artist } from '@app/models/music'
|
||||
import { artistsAtom, artistsUpdatingAtom, useUpdateArtists } from '@app/state/music'
|
||||
import textStyles from '@app/styles/text'
|
||||
import ArtistArt from '@app/components/ArtistArt'
|
||||
import GradientFlatList from '@app/components/GradientFlatList'
|
||||
|
||||
const ArtistItem: React.FC<{ item: Artist }> = ({ item }) => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginVertical: 6,
|
||||
marginLeft: 6,
|
||||
}}
|
||||
onPress={() => navigation.navigate('ArtistView', { id: item.id, title: item.name })}>
|
||||
<ArtistArt id={item.id} width={56} height={56} />
|
||||
<Text
|
||||
style={{
|
||||
...textStyles.paragraph,
|
||||
marginLeft: 12,
|
||||
}}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
const ArtistItemLoader: React.FC<{ item: Artist }> = props => (
|
||||
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||
<ArtistItem {...props} />
|
||||
</React.Suspense>
|
||||
)
|
||||
|
||||
const ArtistsList = () => {
|
||||
const artists = useAtomValue(artistsAtom)
|
||||
const updating = useAtomValue(artistsUpdatingAtom)
|
||||
const updateArtists = useUpdateArtists()
|
||||
|
||||
useEffect(() => {
|
||||
if (artists.length === 0) {
|
||||
updateArtists()
|
||||
}
|
||||
})
|
||||
|
||||
const renderItem: React.FC<{ item: Artist }> = ({ item }) => <ArtistItemLoader item={item} />
|
||||
|
||||
return (
|
||||
<GradientFlatList
|
||||
data={artists}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={item => item.id}
|
||||
onRefresh={updateArtists}
|
||||
refreshing={updating}
|
||||
overScrollMode="never"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const ArtistsTab = () => <ArtistsList />
|
||||
|
||||
export default ArtistsTab
|
||||
6
app/screens/LibraryPlaylists.tsx
Normal file
6
app/screens/LibraryPlaylists.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import React from 'react'
|
||||
import GradientBackground from '@app/components/GradientBackground'
|
||||
|
||||
const PlaylistsTab = () => <GradientBackground />
|
||||
|
||||
export default PlaylistsTab
|
||||
340
app/screens/NowPlayingLayout.tsx
Normal file
340
app/screens/NowPlayingLayout.tsx
Normal file
@@ -0,0 +1,340 @@
|
||||
import { useNavigation } from '@react-navigation/native'
|
||||
import { useAtomValue } from 'jotai/utils'
|
||||
import React, { useEffect } from 'react'
|
||||
import { StatusBar, StyleSheet, Text, View } from 'react-native'
|
||||
import { NativeStackScreenProps } from 'react-native-screens/lib/typescript/native-stack'
|
||||
import { State } from 'react-native-track-player'
|
||||
import IconFA from 'react-native-vector-icons/FontAwesome'
|
||||
import IconFA5 from 'react-native-vector-icons/FontAwesome5'
|
||||
import Icon from 'react-native-vector-icons/Ionicons'
|
||||
import IconMatCom from 'react-native-vector-icons/MaterialCommunityIcons'
|
||||
import IconMat from 'react-native-vector-icons/MaterialIcons'
|
||||
import {
|
||||
currentTrackAtom,
|
||||
playerStateAtom,
|
||||
queueNameAtom,
|
||||
useNext,
|
||||
usePause,
|
||||
usePlay,
|
||||
usePrevious,
|
||||
useProgress,
|
||||
} from '@app/state/trackplayer'
|
||||
import colors from '@app/styles/colors'
|
||||
import { Font } from '@app/styles/text'
|
||||
import formatDuration from '@app/util/formatDuration'
|
||||
import CoverArt from '@app/components/CoverArt'
|
||||
import ImageGradientBackground from '@app/components/ImageGradientBackground'
|
||||
import PressableOpacity from '@app/components/PressableOpacity'
|
||||
|
||||
const NowPlayingHeader = () => {
|
||||
const queueName = useAtomValue(queueNameAtom)
|
||||
const navigation = useNavigation()
|
||||
|
||||
return (
|
||||
<View style={headerStyles.container}>
|
||||
<PressableOpacity onPress={() => navigation.goBack()} style={headerStyles.icons} ripple={true}>
|
||||
<IconMat name="arrow-back" color="white" size={25} />
|
||||
</PressableOpacity>
|
||||
<Text numberOfLines={1} style={headerStyles.queueName}>
|
||||
{queueName || 'Nothing playing...'}
|
||||
</Text>
|
||||
<PressableOpacity onPress={undefined} style={headerStyles.icons} ripple={true}>
|
||||
<IconMat name="more-vert" color="white" size={25} />
|
||||
</PressableOpacity>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const headerStyles = StyleSheet.create({
|
||||
container: {
|
||||
height: 58,
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
icons: {
|
||||
height: 42,
|
||||
width: 42,
|
||||
marginHorizontal: 8,
|
||||
},
|
||||
queueName: {
|
||||
fontFamily: Font.bold,
|
||||
fontSize: 16,
|
||||
color: colors.text.primary,
|
||||
flex: 1,
|
||||
textAlign: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
const SongCoverArt = () => {
|
||||
const track = useAtomValue(currentTrackAtom)
|
||||
|
||||
return (
|
||||
<View style={coverArtStyles.container}>
|
||||
<CoverArt
|
||||
PlaceholderComponent={() => <View style={{ height: '100%', width: '100%' }} />}
|
||||
height={'100%'}
|
||||
width={'100%'}
|
||||
coverArtUri={track?.artwork as string}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const coverArtStyles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
paddingBottom: 20,
|
||||
},
|
||||
})
|
||||
|
||||
const SongInfo = () => {
|
||||
const track = useAtomValue(currentTrackAtom)
|
||||
|
||||
return (
|
||||
<View style={infoStyles.container}>
|
||||
<View style={infoStyles.details}>
|
||||
<Text numberOfLines={1} style={infoStyles.title}>
|
||||
{track?.title}
|
||||
</Text>
|
||||
<Text numberOfLines={1} style={infoStyles.artist}>
|
||||
{track?.artist}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={infoStyles.controls}>
|
||||
<PressableOpacity onPress={undefined}>
|
||||
<IconFA name="star-o" size={32} color={colors.text.secondary} />
|
||||
</PressableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const infoStyles = StyleSheet.create({
|
||||
container: {
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
details: {
|
||||
flex: 1,
|
||||
marginRight: 20,
|
||||
},
|
||||
controls: {
|
||||
justifyContent: 'center',
|
||||
},
|
||||
title: {
|
||||
height: 28,
|
||||
fontFamily: Font.bold,
|
||||
fontSize: 22,
|
||||
color: colors.text.primary,
|
||||
},
|
||||
artist: {
|
||||
height: 20,
|
||||
fontFamily: Font.regular,
|
||||
fontSize: 16,
|
||||
color: colors.text.secondary,
|
||||
},
|
||||
})
|
||||
|
||||
const SeekBar = () => {
|
||||
const { position, duration } = useProgress()
|
||||
|
||||
let progress = 0
|
||||
if (duration > 0) {
|
||||
progress = position / duration
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={seekStyles.container}>
|
||||
<View style={seekStyles.barContainer}>
|
||||
<View style={{ ...seekStyles.bars, ...seekStyles.barLeft, flex: progress }} />
|
||||
<View style={{ ...seekStyles.indicator }} />
|
||||
<View style={{ ...seekStyles.bars, ...seekStyles.barRight, flex: 1 - progress }} />
|
||||
</View>
|
||||
<View style={seekStyles.textContainer}>
|
||||
<Text style={seekStyles.text}>{formatDuration(position)}</Text>
|
||||
<Text style={seekStyles.text}>{formatDuration(duration)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const seekStyles = StyleSheet.create({
|
||||
container: {
|
||||
width: '100%',
|
||||
marginTop: 26,
|
||||
},
|
||||
barContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: 6,
|
||||
},
|
||||
bars: {
|
||||
backgroundColor: colors.text.primary,
|
||||
height: 4,
|
||||
},
|
||||
barLeft: {
|
||||
marginRight: -6,
|
||||
},
|
||||
barRight: {
|
||||
opacity: 0.3,
|
||||
marginLeft: -6,
|
||||
},
|
||||
indicator: {
|
||||
height: 12,
|
||||
width: 12,
|
||||
borderRadius: 6,
|
||||
backgroundColor: colors.text.primary,
|
||||
elevation: 1,
|
||||
},
|
||||
textContainer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
text: {
|
||||
fontFamily: Font.regular,
|
||||
fontSize: 15,
|
||||
color: colors.text.primary,
|
||||
},
|
||||
})
|
||||
|
||||
const PlayerControls = () => {
|
||||
const state = useAtomValue(playerStateAtom)
|
||||
const play = usePlay()
|
||||
const pause = usePause()
|
||||
const next = useNext()
|
||||
const previous = usePrevious()
|
||||
|
||||
let playPauseIcon: string
|
||||
let playPauseAction: undefined | (() => void)
|
||||
let disabled: boolean
|
||||
|
||||
switch (state) {
|
||||
case State.Playing:
|
||||
case State.Buffering:
|
||||
case State.Connecting:
|
||||
disabled = false
|
||||
playPauseIcon = 'pause-circle'
|
||||
playPauseAction = pause
|
||||
break
|
||||
case State.Paused:
|
||||
disabled = false
|
||||
playPauseIcon = 'play-circle'
|
||||
playPauseAction = play
|
||||
break
|
||||
default:
|
||||
disabled = true
|
||||
playPauseIcon = 'play-circle'
|
||||
playPauseAction = undefined
|
||||
break
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={controlsStyles.container}>
|
||||
<View style={controlsStyles.top}>
|
||||
<View style={controlsStyles.center}>
|
||||
<PressableOpacity onPress={undefined} disabled={disabled}>
|
||||
<Icon name="repeat" size={26} color="white" />
|
||||
</PressableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={controlsStyles.center}>
|
||||
<PressableOpacity onPress={previous} disabled={disabled}>
|
||||
<IconFA5 name="step-backward" size={36} color="white" />
|
||||
</PressableOpacity>
|
||||
<PressableOpacity onPress={playPauseAction} disabled={disabled} style={controlsStyles.play}>
|
||||
<IconFA name={playPauseIcon} size={82} color="white" />
|
||||
</PressableOpacity>
|
||||
<PressableOpacity onPress={next} disabled={disabled}>
|
||||
<IconFA5 name="step-forward" size={36} color="white" />
|
||||
</PressableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={controlsStyles.center}>
|
||||
<PressableOpacity onPress={undefined} disabled={disabled}>
|
||||
<Icon name="shuffle" size={26} color="white" />
|
||||
</PressableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
<View style={controlsStyles.bottom}>
|
||||
<PressableOpacity onPress={undefined} disabled={disabled}>
|
||||
<IconMatCom name="cast-audio" size={20} color="white" />
|
||||
</PressableOpacity>
|
||||
<PressableOpacity onPress={undefined} disabled={disabled}>
|
||||
<IconMatCom name="playlist-play" size={24} color="white" />
|
||||
</PressableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const controlsStyles = StyleSheet.create({
|
||||
container: {
|
||||
width: '100%',
|
||||
},
|
||||
top: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingBottom: 8,
|
||||
},
|
||||
bottom: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingTop: 10,
|
||||
paddingBottom: 34,
|
||||
},
|
||||
play: {
|
||||
marginHorizontal: 30,
|
||||
},
|
||||
center: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
type RootStackParamList = {
|
||||
Main: undefined
|
||||
NowPlaying: undefined
|
||||
}
|
||||
type NowPlayingProps = NativeStackScreenProps<RootStackParamList, 'NowPlaying'>
|
||||
|
||||
const NowPlayingLayout: React.FC<NowPlayingProps> = ({ navigation }) => {
|
||||
const track = useAtomValue(currentTrackAtom)
|
||||
|
||||
useEffect(() => {
|
||||
if (!track && navigation.canGoBack()) {
|
||||
navigation.popToTop()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<ImageGradientBackground imageUri={track?.artworkThumb as string} imageKey={`${track?.album}${track?.artist}`} />
|
||||
<NowPlayingHeader />
|
||||
<View style={styles.content}>
|
||||
<SongCoverArt />
|
||||
<SongInfo />
|
||||
<SeekBar />
|
||||
<PlayerControls />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
paddingTop: StatusBar.currentHeight,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
paddingHorizontal: 30,
|
||||
},
|
||||
})
|
||||
|
||||
export default NowPlayingLayout
|
||||
77
app/screens/Settings.tsx
Normal file
77
app/screens/Settings.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useNavigation } from '@react-navigation/core'
|
||||
import { useAtom } from 'jotai'
|
||||
import md5 from 'md5'
|
||||
import React from 'react'
|
||||
import { Button, Text, View } from 'react-native'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { appSettingsAtom } from '@app/state/settings'
|
||||
import { getAllKeys, multiRemove } from '@app/storage/asyncstorage'
|
||||
import text from '@app/styles/text'
|
||||
|
||||
const TestControls = () => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
const removeAllKeys = async () => {
|
||||
const allKeys = await getAllKeys()
|
||||
await multiRemove(allKeys)
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Button title="Remove all keys" onPress={removeAllKeys} />
|
||||
<Button title="Now Playing" onPress={() => navigation.navigate('NowPlaying')} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const ServerSettingsView = () => {
|
||||
const [appSettings, setAppSettings] = useAtom(appSettingsAtom)
|
||||
|
||||
const bootstrapServer = () => {
|
||||
if (appSettings.servers.length !== 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const id = uuidv4()
|
||||
const salt = uuidv4()
|
||||
const address = 'http://demo.subsonic.org'
|
||||
|
||||
setAppSettings({
|
||||
...appSettings,
|
||||
servers: [
|
||||
...appSettings.servers,
|
||||
{
|
||||
id,
|
||||
salt,
|
||||
address,
|
||||
username: 'guest',
|
||||
token: md5('guest' + salt),
|
||||
},
|
||||
],
|
||||
activeServer: id,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Button title="Add default server" onPress={bootstrapServer} />
|
||||
{appSettings.servers.map(s => (
|
||||
<View key={s.id}>
|
||||
<Text style={text.paragraph}>{s.address}</Text>
|
||||
<Text style={text.paragraph}>{s.username}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
const SettingsView = () => (
|
||||
<View>
|
||||
<TestControls />
|
||||
<React.Suspense fallback={<Text>Loading...</Text>}>
|
||||
<ServerSettingsView />
|
||||
</React.Suspense>
|
||||
</View>
|
||||
)
|
||||
|
||||
export default SettingsView
|
||||
45
app/screens/SplashPage.tsx
Normal file
45
app/screens/SplashPage.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { Text, View } from 'react-native'
|
||||
import RNFS from 'react-native-fs'
|
||||
import paths from '@app/util/paths'
|
||||
|
||||
async function mkdir(path: string): Promise<void> {
|
||||
const exists = await RNFS.exists(path)
|
||||
if (exists) {
|
||||
const isDir = (await RNFS.stat(path)).isDirectory()
|
||||
if (!isDir) {
|
||||
throw new Error(`path exists and is not a directory: ${path}`)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return await RNFS.mkdir(path)
|
||||
}
|
||||
|
||||
const SplashPage: React.FC<{}> = ({ children }) => {
|
||||
const [ready, setReady] = useState(false)
|
||||
|
||||
const minSplashTime = new Promise(resolve => setTimeout(resolve, 1))
|
||||
|
||||
const prepare = async () => {
|
||||
await mkdir(paths.imageCache)
|
||||
await mkdir(paths.songCache)
|
||||
await mkdir(paths.songs)
|
||||
}
|
||||
|
||||
const promise = Promise.all([prepare(), minSplashTime])
|
||||
|
||||
useEffect(() => {
|
||||
promise.then(() => {
|
||||
setReady(true)
|
||||
})
|
||||
})
|
||||
|
||||
if (!ready) {
|
||||
return <Text>Loading THE GOOD SHIT...</Text>
|
||||
}
|
||||
return <View style={{ flex: 1 }}>{children}</View>
|
||||
}
|
||||
|
||||
export default SplashPage
|
||||
Reference in New Issue
Block a user