Change modal to native stacks
This commit is contained in:
14
src/screens/modals/Player/components/Casting.android.tsx
Normal file
14
src/screens/modals/Player/components/Casting.android.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/native';
|
||||
|
||||
const Button = styled.View`
|
||||
margin: 20px 40px;
|
||||
`;
|
||||
|
||||
function Casting() {
|
||||
return (
|
||||
<Button />
|
||||
);
|
||||
}
|
||||
|
||||
export default Casting;
|
||||
9
src/screens/modals/Player/components/Casting.d.ts
vendored
Normal file
9
src/screens/modals/Player/components/Casting.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
export interface CastingProps {
|
||||
fill?: string;
|
||||
}
|
||||
|
||||
declare const CastingComponent: React.FC<CastingProps>;
|
||||
|
||||
export default CastingComponent;
|
||||
25
src/screens/modals/Player/components/Casting.ios.tsx
Normal file
25
src/screens/modals/Player/components/Casting.ios.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import React from 'react';
|
||||
import AirPlayButton from 'react-native-airplay-button';
|
||||
import styled from 'styled-components/native';
|
||||
import { CastingProps } from './Casting';
|
||||
|
||||
const Button = styled.View`
|
||||
margin: 20px 40px;
|
||||
`;
|
||||
|
||||
function Casting({ fill }: CastingProps) {
|
||||
return (
|
||||
<>
|
||||
<Button>
|
||||
<AirPlayButton
|
||||
activeTintColor={THEME_COLOR}
|
||||
tintColor={fill}
|
||||
style={{ width: 40, height: 40 }}
|
||||
/>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Casting;
|
||||
37
src/screens/modals/Player/components/ConnectionNotice.tsx
Normal file
37
src/screens/modals/Player/components/ConnectionNotice.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { useNetInfo } from '@react-native-community/netinfo';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import styled from 'styled-components/native';
|
||||
import CloudSlash from 'assets/icons/cloud-slash.svg';
|
||||
import { Text } from 'react-native';
|
||||
import { t } from '@localisation';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
|
||||
const Well = styled.View`
|
||||
border-radius: 8px;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
`;
|
||||
|
||||
function ConnectionNotice() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const { isInternetReachable } = useNetInfo();
|
||||
|
||||
if (!isInternetReachable) {
|
||||
return (
|
||||
<Well style={defaultStyles.activeBackground}>
|
||||
<CloudSlash width={24} height={24} fill={THEME_COLOR} />
|
||||
<Text style={{ color: THEME_COLOR, marginLeft: 12 }}>
|
||||
{t('you-are-offline-message')}
|
||||
</Text>
|
||||
</Well>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default ConnectionNotice;
|
||||
152
src/screens/modals/Player/components/MediaControls.tsx
Normal file
152
src/screens/modals/Player/components/MediaControls.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import TrackPlayer, { Event, State, usePlaybackState, useTrackPlayerEvents } from 'react-native-track-player';
|
||||
import { TouchableOpacity, useColorScheme } from 'react-native';
|
||||
import styled from 'styled-components/native';
|
||||
import { useHasNextQueue, useHasPreviousQueue } from 'utility/useQueue';
|
||||
import ForwardIcon from 'assets/icons/forwards.svg';
|
||||
import BackwardIcon from 'assets/icons/backwards.svg';
|
||||
import PlayIcon from 'assets/icons/play.svg';
|
||||
import PauseIcon from 'assets/icons/pause.svg';
|
||||
import RepeatIcon from 'assets/icons/repeat.svg';
|
||||
// import ShuffleIcon from 'assets/icons/shuffle.svg';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import Casting from './Casting';
|
||||
|
||||
const BUTTON_SIZE = 40;
|
||||
const BUTTON_SIZE_SMALL = 25;
|
||||
|
||||
const pause = TrackPlayer.pause;
|
||||
const play = TrackPlayer.play;
|
||||
const next = TrackPlayer.skipToNext;
|
||||
const previous = TrackPlayer.skipToPrevious;
|
||||
|
||||
const Container = styled.View`
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
`;
|
||||
|
||||
const Buttons = styled.View`
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Button = styled.View`
|
||||
margin: 20px 40px;
|
||||
`;
|
||||
|
||||
export default function MediaControls() {
|
||||
const scheme = useColorScheme();
|
||||
const fill = scheme === 'dark' ? '#ffffff' : '#000000';
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Buttons>
|
||||
<Button>
|
||||
<PreviousButton fill={fill} />
|
||||
</Button>
|
||||
<MainButton fill={fill} />
|
||||
<Button>
|
||||
<NextButton fill={fill} />
|
||||
</Button>
|
||||
</Buttons>
|
||||
<Buttons>
|
||||
<Button>
|
||||
<RepeatButton fill={fill} />
|
||||
</Button>
|
||||
<Casting fill={fill} />
|
||||
</Buttons>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreviousButton({ fill }: { fill: string }) {
|
||||
const hasQueue = useHasPreviousQueue();
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={previous} disabled={!hasQueue} style={{ opacity: hasQueue ? 1 : 0.5 }}>
|
||||
<BackwardIcon width={BUTTON_SIZE} height={BUTTON_SIZE} fill={fill} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
export function NextButton({ fill }: { fill: string }) {
|
||||
const hasQueue = useHasNextQueue();
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={next} disabled={!hasQueue} style={{ opacity: hasQueue ? 1 : 0.5 }}>
|
||||
<ForwardIcon width={BUTTON_SIZE} height={BUTTON_SIZE} fill={fill} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
export function RepeatButton({ fill }: { fill: string}) {
|
||||
const [isRepeating, setRepeating] = useState(false);
|
||||
const handlePress = useCallback(() => setRepeating(!isRepeating), [isRepeating, setRepeating]);
|
||||
|
||||
// The callback that should determine whether we need to repeeat or not
|
||||
useTrackPlayerEvents([Event.PlaybackQueueEnded], async () => {
|
||||
if (isRepeating) {
|
||||
// Skip to the first track
|
||||
await TrackPlayer.skip(0);
|
||||
|
||||
// Cautiously reset the seek time, as there might only be a single
|
||||
// item in queue.
|
||||
await TrackPlayer.seekTo(0);
|
||||
|
||||
// Then play the item
|
||||
await TrackPlayer.play();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={handlePress} style={{ opacity: isRepeating ? 1 : 0.5 }}>
|
||||
<RepeatIcon
|
||||
width={BUTTON_SIZE_SMALL}
|
||||
height={BUTTON_SIZE_SMALL}
|
||||
fill={isRepeating ? THEME_COLOR : fill}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
// export function ShuffleButton({ fill }: { fill: string}) {
|
||||
// const [isShuffling, setShuffling] = useState(false);
|
||||
// const handlePress = useCallback(() => setShuffling(!isShuffling), [isShuffling, setShuffling]);
|
||||
|
||||
// return (
|
||||
// <TouchableOpacity onPress={handlePress} style={{ opacity: isShuffling ? 1 : 0.5 }}>
|
||||
// <ShuffleIcon
|
||||
// width={BUTTON_SIZE_SMALL}
|
||||
// height={BUTTON_SIZE_SMALL}
|
||||
// fill={isShuffling ? THEME_COLOR : fill}
|
||||
// />
|
||||
// </TouchableOpacity>
|
||||
// );
|
||||
// }
|
||||
|
||||
export function MainButton({ fill }: { fill: string }) {
|
||||
const state = usePlaybackState();
|
||||
|
||||
switch (state) {
|
||||
case State.Playing:
|
||||
return (
|
||||
<TouchableOpacity onPress={pause}>
|
||||
<PauseIcon width={BUTTON_SIZE} height={BUTTON_SIZE} fill={fill} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
case State.Paused:
|
||||
return (
|
||||
<TouchableOpacity onPress={play}>
|
||||
<PlayIcon width={BUTTON_SIZE} height={BUTTON_SIZE} fill={fill} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<TouchableOpacity onPress={pause} disabled>
|
||||
<PauseIcon width={BUTTON_SIZE} height={BUTTON_SIZE} fill={fill} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
51
src/screens/modals/Player/components/NowPlaying.tsx
Normal file
51
src/screens/modals/Player/components/NowPlaying.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { Dimensions, View, StyleSheet } from 'react-native';
|
||||
import useCurrentTrack from 'utility/useCurrentTrack';
|
||||
import styled from 'styled-components/native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
import Text from 'components/Text';
|
||||
|
||||
const Screen = Dimensions.get('screen');
|
||||
|
||||
const Artwork = styled(FastImage)`
|
||||
border-radius: 10px;
|
||||
width: ${Screen.width * 0.8}px;
|
||||
height: ${Screen.width * 0.8}px;
|
||||
margin: 25px auto;
|
||||
`;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
artist: {
|
||||
fontWeight: 'bold',
|
||||
fontSize: 24,
|
||||
marginBottom: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
marginBottom: 12,
|
||||
textAlign: 'center',
|
||||
paddingLeft: 20,
|
||||
paddingRight: 20,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export default function NowPlaying() {
|
||||
const { track } = useCurrentTrack();
|
||||
const defaultStyles = useDefaultStyles();
|
||||
|
||||
return (
|
||||
<View style={{ alignItems: 'center' }}>
|
||||
<Artwork
|
||||
style={defaultStyles.imageBackground}
|
||||
source={{
|
||||
uri: track?.artwork as string | undefined,
|
||||
priority: FastImage.priority.high,
|
||||
}}
|
||||
/>
|
||||
<Text style={styles.artist}>{track?.artist}</Text>
|
||||
<Text style={styles.title}>{track?.title}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
95
src/screens/modals/Player/components/ProgressBar.tsx
Normal file
95
src/screens/modals/Player/components/ProgressBar.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React, { Component } from 'react';
|
||||
import TrackPlayer from 'react-native-track-player';
|
||||
import styled from 'styled-components/native';
|
||||
import { Text, Platform } from 'react-native';
|
||||
import Slider from '@react-native-community/slider';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import { DefaultStylesProvider } from 'components/Colors';
|
||||
|
||||
const NumberBar = styled.View`
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 20px 0;
|
||||
`;
|
||||
|
||||
function getSeconds(seconds: number): string {
|
||||
return Math.floor(seconds % 60).toString().padStart(2, '0');
|
||||
}
|
||||
|
||||
function getMinutes(seconds: number): number {
|
||||
return Math.floor(seconds / 60);
|
||||
}
|
||||
|
||||
interface State {
|
||||
position: number;
|
||||
duration: number;
|
||||
gesture?: number;
|
||||
}
|
||||
|
||||
export default class ProgressBar extends Component<{}, State> {
|
||||
state: State = {
|
||||
position: 0,
|
||||
duration: 0,
|
||||
};
|
||||
|
||||
timer: number | null = null;
|
||||
|
||||
componentDidMount() {
|
||||
this.timer = setInterval(this.updateProgress, 500);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress = async () => {
|
||||
const [position, duration] = await Promise.all([
|
||||
TrackPlayer.getPosition(),
|
||||
TrackPlayer.getDuration(),
|
||||
]);
|
||||
|
||||
this.setState({ position, duration });
|
||||
};
|
||||
|
||||
handleGesture = async (gesture: number) => {
|
||||
// Set relative translation in state
|
||||
this.setState({ gesture });
|
||||
};
|
||||
|
||||
handleEndOfGesture = (position: number) => {
|
||||
// Calculate and set the new position
|
||||
TrackPlayer.seekTo(position);
|
||||
this.setState({ gesture: undefined, position });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { position, duration, gesture } = this.state;
|
||||
|
||||
return (
|
||||
<DefaultStylesProvider>
|
||||
{defaultStyle => (
|
||||
<>
|
||||
<Slider
|
||||
value={gesture || position}
|
||||
minimumValue={0}
|
||||
maximumValue={duration || 0}
|
||||
onValueChange={this.handleGesture}
|
||||
onSlidingComplete={this.handleEndOfGesture}
|
||||
minimumTrackTintColor={THEME_COLOR}
|
||||
thumbTintColor={Platform.OS === 'android' ? THEME_COLOR : undefined}
|
||||
disabled={!duration}
|
||||
/>
|
||||
<NumberBar>
|
||||
<Text style={defaultStyle.text}>{getMinutes(gesture || position)}:{getSeconds(gesture || position)}</Text>
|
||||
<Text style={defaultStyle.text}>{getMinutes(duration)}:{getSeconds(duration)}</Text>
|
||||
</NumberBar>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</DefaultStylesProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
84
src/screens/modals/Player/components/Queue.tsx
Normal file
84
src/screens/modals/Player/components/Queue.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import useQueue from 'utility/useQueue';
|
||||
import { View, StyleSheet } from 'react-native';
|
||||
import styled, { css } from 'styled-components/native';
|
||||
import useCurrentTrack from 'utility/useCurrentTrack';
|
||||
import TouchableHandler from 'components/TouchableHandler';
|
||||
import TrackPlayer from 'react-native-track-player';
|
||||
import { t } from '@localisation';
|
||||
import useDefaultStyles from 'components/Colors';
|
||||
import Text from 'components/Text';
|
||||
import Button from 'components/Button';
|
||||
import { THEME_COLOR } from 'CONSTANTS';
|
||||
import DownloadIcon from 'components/DownloadIcon';
|
||||
|
||||
const QueueItem = styled.View<{ active?: boolean, alreadyPlayed?: boolean, isDark?: boolean }>`
|
||||
padding: 10px;
|
||||
border-bottom-width: 1px;
|
||||
flex: 0 0 auto;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
${props => props.active && css`
|
||||
font-weight: 900;
|
||||
padding: 20px 35px;
|
||||
margin: 0 -25px;
|
||||
`}
|
||||
|
||||
${props => props.alreadyPlayed && css`
|
||||
opacity: 0.5;
|
||||
`}
|
||||
`;
|
||||
|
||||
const ClearQueue = styled.View`
|
||||
margin: 20px 0;
|
||||
`;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
trackTitle: {
|
||||
marginBottom: 2
|
||||
}
|
||||
});
|
||||
|
||||
export default function Queue() {
|
||||
const defaultStyles = useDefaultStyles();
|
||||
const queue = useQueue();
|
||||
const { index: currentIndex } = useCurrentTrack();
|
||||
const playTrack = useCallback(async (index: number) => {
|
||||
await TrackPlayer.skip(index);
|
||||
await TrackPlayer.play();
|
||||
}, []);
|
||||
const clearQueue = useCallback(async () => {
|
||||
await TrackPlayer.reset();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text style={{ marginTop: 20, marginBottom: 20 }}>{t('queue')}</Text>
|
||||
{queue.map((track, i) => (
|
||||
<TouchableHandler id={i} onPress={playTrack} key={i}>
|
||||
<QueueItem
|
||||
active={currentIndex === i}
|
||||
key={i}
|
||||
alreadyPlayed={currentIndex ? i < currentIndex : false}
|
||||
style={[
|
||||
defaultStyles.border,
|
||||
currentIndex === i ? defaultStyles.activeBackground : {},
|
||||
]}
|
||||
>
|
||||
<View>
|
||||
<Text style={currentIndex === i ? { color: THEME_COLOR, fontWeight: '700' } : styles.trackTitle}>{track.title}</Text>
|
||||
<Text style={currentIndex === i ? { color: THEME_COLOR, fontWeight: '400' } : defaultStyles.textHalfOpacity}>{track.artist}</Text>
|
||||
</View>
|
||||
<View style={{ marginLeft: 'auto' }}>
|
||||
<DownloadIcon trackId={track.backendId} />
|
||||
</View>
|
||||
</QueueItem>
|
||||
</TouchableHandler>
|
||||
))}
|
||||
<ClearQueue>
|
||||
<Button title={t('clear-queue')} onPress={clearQueue} />
|
||||
</ClearQueue>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user