Change modal to native stacks

This commit is contained in:
Lei Nelissen
2022-05-04 19:12:01 +02:00
parent 2b24a37218
commit 76f2db19e5
15 changed files with 78 additions and 16 deletions

View 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;

View File

@@ -0,0 +1,9 @@
import React from 'react';
export interface CastingProps {
fill?: string;
}
declare const CastingComponent: React.FC<CastingProps>;
export default CastingComponent;

View 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;

View 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;

View 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>
);
}
}

View 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>
);
}

View 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>
);
}
}

View 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>
);
}