some progress on now playing

image colors gradient working for now playing
fixed track player init losing handle to notification
This commit is contained in:
austinried 2021-07-02 16:39:07 +09:00
parent 28ab092402
commit 0a31597111
12 changed files with 278 additions and 190 deletions

14
App.tsx
View File

@ -3,7 +3,7 @@ import { DarkTheme, NavigationContainer } from '@react-navigation/native';
import SplashPage from './src/components/SplashPage';
import RootNavigator from './src/components/navigation/RootNavigator';
import { Provider } from 'jotai';
import { StatusBar } from 'react-native';
import { StatusBar, View } from 'react-native';
import colors from './src/styles/colors';
import TrackPlayerState from './src/components/TrackPlayerState';
@ -14,11 +14,13 @@ const App = () => (
<Provider>
<StatusBar animated={true} backgroundColor={'rgba(0, 0, 0, 0.4)'} barStyle={'light-content'} translucent={true} />
<TrackPlayerState />
<SplashPage>
<NavigationContainer theme={theme}>
<RootNavigator />
</NavigationContainer>
</SplashPage>
<View style={{ flex: 1, backgroundColor: colors.gradient.high }}>
<SplashPage>
<NavigationContainer theme={theme}>
<RootNavigator />
</NavigationContainer>
</SplashPage>
</View>
</Provider>
);

View File

@ -7,7 +7,22 @@ enableScreens();
import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';
import TrackPlayer from 'react-native-track-player';
import TrackPlayer, { Capability } from 'react-native-track-player';
AppRegistry.registerComponent(appName, () => App);
TrackPlayer.registerPlaybackService(() => require('./src/playback/service'));
async function start() {
await TrackPlayer.setupPlayer();
await TrackPlayer.updateOptions({
capabilities: [
Capability.Play,
Capability.Pause,
Capability.Stop,
Capability.SkipToNext,
Capability.SkipToPrevious,
],
compactCapabilities: [Capability.Play, Capability.Pause, Capability.SkipToNext, Capability.SkipToPrevious],
});
}
start();

View File

@ -25,6 +25,7 @@
"react-native-fs": "^2.18.0",
"react-native-gesture-handler": "^1.10.3",
"react-native-get-random-values": "^1.7.0",
"react-native-image-colors": "^1.3.0",
"react-native-linear-gradient": "^2.5.6",
"react-native-reanimated": "^2.2.0",
"react-native-safe-area-context": "^3.2.0",

View File

@ -1,134 +1,182 @@
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { useAtomValue } from 'jotai/utils';
import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, View, StatusBar, useWindowDimensions } from 'react-native';
import FastImage from 'react-native-fast-image';
import { currentQueueNameAtom, currentTrackAtom } from '../state/trackplayer';
import colors from '../styles/colors';
import text from '../styles/text';
import CoverArt from './common/CoverArt';
import GradientBackground from './common/GradientBackground';
import ImageColors from 'react-native-image-colors';
const NowPlayingHeader = () => {
const queueName = useAtomValue(currentQueueNameAtom);
const NowPlayingLayout = () => {
return (
<View
style={{
// background
backgroundColor: 'darkblue',
flex: 1,
}}>
{/* top bar */}
<View
style={{
height: 70,
flexDirection: 'row',
}}>
<View style={{ width: 70, height: 70, backgroundColor: 'grey' }} />
<View style={{ flex: 1, alignItems: 'center', height: 70 }}>
<View style={{ flex: 1 }} />
<Text style={styles.text}>Playing from Your Library</Text>
<Text style={styles.text}>Songs</Text>
<View style={{ flex: 1 }} />
</View>
<View style={{ width: 70, height: 70, backgroundColor: 'grey' }} />
</View>
{/* album art */}
<View
style={{
flex: 5,
// backgroundColor: 'darkorange',
alignItems: 'center',
}}>
<View style={{ flex: 1 }} />
<View
style={{
width: 320,
height: 320,
backgroundColor: 'grey',
}}
/>
<View style={{ flex: 1 }} />
</View>
{/* song/album/artist title */}
<View
style={{
flex: 1,
// backgroundColor: 'green',
alignItems: 'center',
}}>
<Text style={{ ...styles.text, fontSize: 26 }}>Name of the Song</Text>
<Text style={{ ...styles.text, fontSize: 20, fontWeight: 'normal' }}>Cool Artist</Text>
</View>
{/* seek bar */}
<View
style={{
flex: 0.7,
// backgroundColor: 'red',
flexDirection: 'row',
alignItems: 'center',
}}>
<View style={{ width: 20 }} />
<View
style={{
flex: 1,
}}>
<View>
<View
style={{
backgroundColor: 'grey',
height: 3,
marginBottom: 3,
// flex: 1,
}}
/>
<View
style={{
flexDirection: 'row',
}}>
<Text style={{ ...styles.text, fontWeight: 'normal' }}>00:00</Text>
<View style={{ flex: 1 }} />
<Text style={{ ...styles.text, fontWeight: 'normal' }}>00:00</Text>
</View>
</View>
</View>
<View style={{ width: 20 }} />
</View>
{/* main player controls */}
<View
style={{
height: 90,
// backgroundColor: 'darkorange',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
}}>
<View style={{ width: 14 }} />
<View style={{ width: 60, height: 60, backgroundColor: 'grey' }} />
<View style={{ width: 60, height: 60, backgroundColor: 'grey' }} />
<View style={{ width: 90, height: 90, backgroundColor: 'grey' }} />
<View style={{ width: 60, height: 60, backgroundColor: 'grey' }} />
<View style={{ width: 60, height: 60, backgroundColor: 'grey' }} />
<View style={{ width: 14 }} />
</View>
{/* extra controls */}
<View
style={{
flex: 1,
// backgroundColor: 'green',
flexDirection: 'row',
}}>
<View style={{ width: 14 }} />
<View style={{ width: 60, height: 60, backgroundColor: 'grey' }} />
<View style={{ flex: 1 }} />
<View style={{ width: 60, height: 60, backgroundColor: 'grey' }} />
<View style={{ width: 14 }} />
</View>
<View style={headerStyles.container}>
<FastImage source={require('../../res/arrow_left-fill.png')} style={headerStyles.backArrow} tintColor="white" />
<Text numberOfLines={2} style={headerStyles.queueName}>
{queueName}
</Text>
<FastImage source={require('../../res/more_vertical.png')} style={headerStyles.more} tintColor="white" />
</View>
);
};
const styles = StyleSheet.create({
text: {
color: 'white',
fontWeight: 'bold',
const headerStyles = StyleSheet.create({
container: {
height: 60,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
// backgroundColor: 'green',
},
backArrow: {
height: 24,
width: 24,
margin: 20,
},
queueName: {
...text.paragraph,
},
more: {
height: 24,
width: 24,
margin: 20,
},
});
const SongCoverArt = () => {
const track = useAtomValue(currentTrackAtom);
const layout = useWindowDimensions();
const size = layout.width - layout.width / 6;
return (
<View style={coverArtStyles.container}>
<CoverArt
PlaceholderComponent={() => (
<View style={{ height: size, width: size }}>
<Text>Failed</Text>
</View>
)}
height={size}
width={size}
coverArtUri={track?.artwork as string}
/>
</View>
);
};
const coverArtStyles = StyleSheet.create({
container: {
width: '100%',
alignItems: 'center',
marginTop: 20,
},
});
const SongInfo = () => {
const track = useAtomValue(currentTrackAtom);
return (
<View style={infoStyles.container}>
<Text style={infoStyles.title}>{track?.title}</Text>
<Text style={infoStyles.artist}>{track?.artist}</Text>
</View>
);
};
const infoStyles = StyleSheet.create({
container: {
width: '100%',
alignItems: 'center',
marginTop: 20,
paddingHorizontal: 20,
},
title: {
...text.songListTitle,
fontSize: 22,
textAlign: 'center',
},
artist: {
...text.songListSubtitle,
fontSize: 14,
textAlign: 'center',
},
});
interface AndroidImageColors {
dominant?: string;
average?: string;
vibrant?: string;
darkVibrant?: string;
lightVibrant?: string;
darkMuted?: string;
lightMuted?: string;
muted?: string;
platform: 'android';
}
interface IOSImageColors {
background: string;
primary: string;
secondary: string;
detail: string;
quality: Config['quality'];
platform: 'ios';
}
interface Config {
fallback?: string;
pixelSpacing?: number;
quality?: 'lowest' | 'low' | 'high' | 'highest';
cache?: boolean;
key?: string;
}
declare type ImageColorsResult = AndroidImageColors | IOSImageColors;
const NowPlayingLayout = () => {
const track = useAtomValue(currentTrackAtom);
const [imageColors, setImageColors] = useState<ImageColorsResult | undefined>(undefined);
const ica = imageColors as AndroidImageColors;
useEffect(() => {
async function getColors() {
if (track?.artwork === undefined) {
return;
}
const cachedResult = ImageColors.cache.getItem(track.artwork as string);
if (cachedResult) {
setImageColors(cachedResult);
return;
}
const result = await ImageColors.getColors(track.artwork as string, {
cache: true,
});
setImageColors(result);
}
getColors();
}, [track]);
return (
<View
style={{
flex: 1,
paddingTop: StatusBar.currentHeight,
}}>
<GradientBackground
colors={[ica ? (ica.muted as string) : colors.gradient.high, colors.gradient.low]}
locations={[0.1, 1.0]}
/>
<NowPlayingHeader />
<SongCoverArt />
<SongInfo />
</View>
);
};
export default NowPlayingLayout;

View File

@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Text, View } from 'react-native';
import RNFS from 'react-native-fs';
import TrackPlayer, { Capability, Track } from 'react-native-track-player';
import paths from '../paths';
async function mkdir(path: string): Promise<void> {
@ -27,32 +26,6 @@ const SplashPage: React.FC<{}> = ({ children }) => {
await mkdir(paths.imageCache);
await mkdir(paths.songCache);
await mkdir(paths.songs);
await TrackPlayer.setupPlayer();
TrackPlayer.updateOptions({
capabilities: [
Capability.Play,
Capability.Pause,
Capability.Stop,
Capability.SkipToNext,
Capability.SkipToPrevious,
],
compactCapabilities: [Capability.Play, Capability.Pause, Capability.SkipToNext, Capability.SkipToPrevious],
});
const castlevania: Track = {
id: 'castlevania',
url: 'http://www.vgmuseum.com/mrp/cv1/music/03.mp3',
title: 'Stage 1: Castle Entrance',
artist: 'Kinuyo Yamashita and S.Terishima',
duration: 110,
artwork: 'https://webgames.host/uploads/2017/03/castlevania-3-draculas-curse.jpg',
genre: 'BGM',
date: new Date(1989, 1).toISOString(),
};
await TrackPlayer.add([castlevania]);
// TrackPlayer.play();
};
const promise = Promise.all([prepare(), minSplashTime]);

View File

@ -1,15 +1,15 @@
import React, { useCallback, useEffect } from 'react';
import TrackPlayer, { Event, State, Track, useTrackPlayerEvents } from 'react-native-track-player';
import TrackPlayer, { Event, useTrackPlayerEvents } from 'react-native-track-player';
import { useAppState } from '@react-native-community/hooks';
import { useUpdateAtom, useAtomValue } from 'jotai/utils';
import { currentTrackAtom } from '../state/trackplayer';
import { currentQueueNameAtom, currentTrackAtom } from '../state/trackplayer';
import { View } from 'react-native';
const TrackPlayerState = () => {
const CurrentTrackState = () => {
const setCurrentTrack = useUpdateAtom(currentTrackAtom);
const appState = useAppState();
const updateCurrentTrack = useCallback(async () => {
const update = useCallback(async () => {
const index = await TrackPlayer.getCurrentTrack();
if (index !== null && index >= 0) {
@ -39,34 +39,59 @@ const TrackPlayerState = () => {
setCurrentTrack(undefined);
return;
}
updateCurrentTrack();
update();
},
);
useEffect(() => {
if (appState === 'active') {
updateCurrentTrack();
update();
}
}, [appState, updateCurrentTrack]);
}, [appState, update]);
return <></>;
};
const CurrentTrack = () => {
const currentTrack = useAtomValue(currentTrackAtom);
const CurrentQueueName = () => {
const setCurrentQueueName = useUpdateAtom(currentQueueNameAtom);
const appState = useAppState();
const update = useCallback(async () => {
const queue = await TrackPlayer.getQueue();
if (queue !== null && queue.length > 0) {
setCurrentQueueName(queue[0].queueName);
return;
}
setCurrentQueueName(undefined);
}, [setCurrentQueueName]);
useEffect(() => {
console.log(currentTrack?.title);
}, [currentTrack]);
if (appState === 'active') {
update();
}
}, [appState, update]);
return <></>;
};
const ASDFSADFSAF = () => (
const Debug = () => {
const value = useAtomValue(currentQueueNameAtom);
useEffect(() => {
console.log(value);
}, [value]);
return <></>;
};
const TrackPlayerState = () => (
<View>
<TrackPlayerState />
<CurrentTrack />
<CurrentTrackState />
<CurrentQueueName />
<Debug />
</View>
);
export default ASDFSADFSAF;
export default TrackPlayerState;

View File

@ -10,7 +10,7 @@ import {
useWindowDimensions,
View,
} from 'react-native';
import { useSetQueue } from '../../hooks/player';
import { useSetQueue } from '../../hooks/trackplayer';
import { albumAtomFamily } from '../../state/music';
import { currentTrackAtom } from '../../state/trackplayer';
import colors from '../../styles/colors';
@ -138,7 +138,7 @@ const AlbumDetails: React.FC<{
style={{
flexDirection: 'row',
}}>
<Button title="Play Album" onPress={() => setQueue(album.songs, album.songs[0].id)} />
<Button title="Play Album" onPress={() => setQueue(album.songs, album.name, album.songs[0].id)} />
</View>
<View
@ -162,7 +162,7 @@ const AlbumDetails: React.FC<{
title={s.title}
artist={s.artist}
track={s.track}
onPress={() => setQueue(album.songs, s.id)}
onPress={() => setQueue(album.songs, album.name, s.id)}
/>
))}
</View>

View File

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

View File

@ -6,9 +6,12 @@ import BottomTabNavigator from './BottomTabNavigator';
const RootStack = createNativeStackNavigator();
const RootNavigator = () => (
<RootStack.Navigator>
<RootStack.Screen name="Main" component={BottomTabNavigator} options={{ headerShown: false }} />
<RootStack.Screen name="Now Playing" component={NowPlayingLayout} options={{ headerShown: false }} />
<RootStack.Navigator
screenOptions={{
headerShown: false,
}}>
<RootStack.Screen name="Main" component={BottomTabNavigator} />
<RootStack.Screen name="Now Playing" component={NowPlayingLayout} />
</RootStack.Navigator>
);

View File

@ -1,11 +1,12 @@
import { useUpdateAtom } from 'jotai/utils';
import TrackPlayer, { Track } from 'react-native-track-player';
import { Song } from '../models/music';
import { currentTrackAtom } from '../state/trackplayer';
import { currentQueueNameAtom, currentTrackAtom } from '../state/trackplayer';
function mapSongToTrack(song: Song): Track {
function mapSongToTrack(song: Song, queueName: string): Track {
return {
id: song.id,
queueName,
title: song.title,
artist: song.artist || 'Unknown Artist',
url: song.streamUri,
@ -16,11 +17,13 @@ function mapSongToTrack(song: Song): Track {
export const useSetQueue = () => {
const setCurrentTrack = useUpdateAtom(currentTrackAtom);
const setCurrentQueueName = useUpdateAtom(currentQueueNameAtom);
return async (songs: Song[], playId?: string) => {
return async (songs: Song[], name: string, playId?: string) => {
await TrackPlayer.reset();
const tracks = songs.map(mapSongToTrack);
const tracks = songs.map(s => mapSongToTrack(s, name));
setCurrentQueueName(name);
if (playId) {
setCurrentTrack(tracks.find(t => t.id === playId));
}

View File

@ -8,9 +8,20 @@ const currentTrack = atom<OptionalTrack>(undefined);
export const currentTrackAtom = atom<OptionalTrack, OptionalTrack>(
get => get(currentTrack),
(get, set, value) => {
if (equal(get(currentTrack), value)) {
return;
if (!equal(get(currentTrack), value)) {
set(currentTrack, value);
}
},
);
type OptionalString = string | undefined;
const currentQueueName = atom<OptionalString>(undefined);
export const currentQueueNameAtom = atom<OptionalString, OptionalString>(
get => get(currentQueueName),
(get, set, value) => {
if (get(currentQueueName) !== value) {
set(currentQueueName, value);
}
set(currentTrack, value);
},
);

View File

@ -5429,6 +5429,11 @@ react-native-get-random-values@^1.7.0:
dependencies:
fast-base64-decode "^1.0.0"
react-native-image-colors@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/react-native-image-colors/-/react-native-image-colors-1.3.0.tgz#3e499730618540fddb779b3f73defe81d2d91801"
integrity sha512-1k+4wXWqm+sFA3H01Xri9HQ4UCWi0dvuUID4rrnzu6VlR/Oa525Scot0li36IOTOl0x/Ar883bh0bcfHOhi+vg==
react-native-iphone-x-helper@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/react-native-iphone-x-helper/-/react-native-iphone-x-helper-1.3.1.tgz#20c603e9a0e765fd6f97396638bdeb0e5a60b010"