mirror of
https://github.com/tsirysndr/music-player.git
synced 2026-01-08 20:58:07 -05:00
[mobile] fetch albums, artists and tracks from API
This commit is contained in:
@@ -103,13 +103,8 @@ impl LibraryQuery {
|
||||
.collect());
|
||||
}
|
||||
|
||||
let db = ctx.data::<Database>().unwrap();
|
||||
let results = ArtistRepository::new(db.get_connection())
|
||||
.find_all(filter, offset.map(|x| x as u64), limit.map(|x| x as u64))
|
||||
.await?;
|
||||
|
||||
Ok(results.into_iter().map(Into::into).collect())
|
||||
}
|
||||
let db = ctx.data::<Database>().unwrap();
|
||||
let results = ArtistRepository::new(db.get_connection());
|
||||
|
||||
async fn albums(
|
||||
&self,
|
||||
|
||||
89
mobile/src/Components/AlbumRow/AlbumRow.tsx
Normal file
89
mobile/src/Components/AlbumRow/AlbumRow.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import React, {FC} from 'react';
|
||||
import {Album} from '../../Types';
|
||||
import styled from '@emotion/native';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
import {TouchableWithoutFeedback} from 'react-native';
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import {useCover} from '../../Hooks/useCover';
|
||||
|
||||
const Container = styled.View`
|
||||
height: 80px;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
`;
|
||||
|
||||
const Cover = styled.Image`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
`;
|
||||
|
||||
const NoAlbumCover = styled.View`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background-color: #161515;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const Title = styled.Text`
|
||||
color: #fff;
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
const Artist = styled.Text`
|
||||
color: #a7a7a9;
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
`;
|
||||
|
||||
const AlbumInfo = styled.View`
|
||||
flex-direction: column;
|
||||
margin-left: 15px;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const Button = styled.TouchableOpacity`
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export type AlbumRowProps = {
|
||||
album: Album;
|
||||
onSelected: () => void;
|
||||
};
|
||||
|
||||
const AlbumRow: FC<AlbumRowProps> = props => {
|
||||
const {album, onSelected} = props;
|
||||
const cover = useCover(album.cover);
|
||||
return (
|
||||
<TouchableWithoutFeedback onPress={() => onSelected()}>
|
||||
<Container>
|
||||
{album.cover && <Cover source={{uri: cover}} />}
|
||||
{!album.cover && (
|
||||
<NoAlbumCover>
|
||||
<Feather name="disc" size={40} color="#a7a7a9" />
|
||||
</NoAlbumCover>
|
||||
)}
|
||||
<AlbumInfo>
|
||||
<Title numberOfLines={1} ellipsizeMode="tail">
|
||||
{album.title}
|
||||
</Title>
|
||||
<Artist numberOfLines={1} ellipsizeMode="tail">
|
||||
{album.artist}
|
||||
</Artist>
|
||||
</AlbumInfo>
|
||||
<Button>
|
||||
<Ionicons name="ellipsis-vertical" color={'#ffffff99'} size={18} />
|
||||
</Button>
|
||||
</Container>
|
||||
</TouchableWithoutFeedback>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlbumRow;
|
||||
175
mobile/src/Components/Albums/Albums.tsx
Normal file
175
mobile/src/Components/Albums/Albums.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import styled from '@emotion/native';
|
||||
import React, {FC} from 'react';
|
||||
import {FlatList, TouchableWithoutFeedback} from 'react-native';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
import {useCover} from '../../Hooks/useCover';
|
||||
|
||||
const Container = styled.View`
|
||||
width: 100%;
|
||||
margin-bottom: 50px;
|
||||
`;
|
||||
|
||||
const Placeholder = styled.View`
|
||||
width: 100%;
|
||||
height: 228px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const PlaceholderText = styled.Text`
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 16px;
|
||||
color: #a7a7a9;
|
||||
`;
|
||||
|
||||
const Header = styled.View`
|
||||
margin: 0 20px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const Title = styled.Text`
|
||||
color: #fff;
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 20px;
|
||||
`;
|
||||
|
||||
const NoAlbumCover = styled.View`
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background-color: #161515;
|
||||
margin-right: 8px;
|
||||
margin-left: 8px;
|
||||
border-radius: 3px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const AlbumCover = styled.View`
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const Cover = styled.Image`
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
margin-right: 8px;
|
||||
margin-left: 8px;
|
||||
border-radius: 3px;
|
||||
`;
|
||||
|
||||
const AlbumTitle = styled.Text`
|
||||
color: #fff;
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 16px;
|
||||
width: 180px;
|
||||
`;
|
||||
|
||||
const Artist = styled.Text`
|
||||
color: #a7a7a9;
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 14px;
|
||||
width: 180px;
|
||||
`;
|
||||
|
||||
const AlbumInfo = styled.View`
|
||||
margin-left: 8px;
|
||||
margin-top: 10px;
|
||||
`;
|
||||
|
||||
const SeeAll = styled.Text`
|
||||
color: #a7a7a9;
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
export type AlbumProps = {
|
||||
album: any;
|
||||
onPress: (album: any) => void;
|
||||
};
|
||||
|
||||
const Album: FC<AlbumProps> = ({album, onPress}) => {
|
||||
const cover = useCover(album.cover);
|
||||
return (
|
||||
<>
|
||||
{!album.cover && (
|
||||
<TouchableWithoutFeedback onPress={() => onPress(album)}>
|
||||
<AlbumCover>
|
||||
<NoAlbumCover>
|
||||
<Feather name="disc" size={100} color="#bdb9b958" />
|
||||
</NoAlbumCover>
|
||||
<AlbumInfo>
|
||||
<AlbumTitle ellipsizeMode="tail" numberOfLines={1}>
|
||||
{album.title}
|
||||
</AlbumTitle>
|
||||
<Artist ellipsizeMode="tail" numberOfLines={2}>
|
||||
{album.artist}
|
||||
</Artist>
|
||||
</AlbumInfo>
|
||||
</AlbumCover>
|
||||
</TouchableWithoutFeedback>
|
||||
)}
|
||||
{album.cover && (
|
||||
<TouchableWithoutFeedback onPress={() => onPress(album)}>
|
||||
<AlbumCover>
|
||||
<Cover
|
||||
source={{
|
||||
uri: cover,
|
||||
}}
|
||||
/>
|
||||
<AlbumInfo>
|
||||
<AlbumTitle ellipsizeMode="tail" numberOfLines={1}>
|
||||
{album.title}
|
||||
</AlbumTitle>
|
||||
<Artist ellipsizeMode="tail" numberOfLines={2}>
|
||||
{album.artist}
|
||||
</Artist>
|
||||
</AlbumInfo>
|
||||
</AlbumCover>
|
||||
</TouchableWithoutFeedback>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export type AlbumsProps = {
|
||||
albums: any;
|
||||
onSeeAll: () => void;
|
||||
onAlbumPress: (album: any) => void;
|
||||
};
|
||||
|
||||
const Albums: FC<AlbumsProps> = props => {
|
||||
const {albums, onSeeAll, onAlbumPress} = props;
|
||||
return (
|
||||
<Container>
|
||||
<Header>
|
||||
<Title>Albums</Title>
|
||||
{albums.length > 0 && (
|
||||
<TouchableWithoutFeedback onPress={onSeeAll}>
|
||||
<SeeAll>See All</SeeAll>
|
||||
</TouchableWithoutFeedback>
|
||||
)}
|
||||
</Header>
|
||||
<>
|
||||
{albums.length > 0 && (
|
||||
<FlatList
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
data={albums}
|
||||
renderItem={({item}) => (
|
||||
<Album key={item.id} album={item} onPress={onAlbumPress} />
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{albums.length === 0 && (
|
||||
<Placeholder>
|
||||
<PlaceholderText>No Albums</PlaceholderText>
|
||||
</Placeholder>
|
||||
)}
|
||||
</>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Albums;
|
||||
26
mobile/src/Components/Albums/AlbumsWithData.tsx
Normal file
26
mobile/src/Components/Albums/AlbumsWithData.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, {FC} from 'react';
|
||||
import Albums from './Albums';
|
||||
import {useGetAlbumsQuery} from '../../Hooks/GraphQL';
|
||||
|
||||
const AlbumsWithData: FC = () => {
|
||||
const {data, loading} = useGetAlbumsQuery({
|
||||
variables: {
|
||||
limit: 10,
|
||||
},
|
||||
});
|
||||
const albums = !loading && data ? data.albums : [];
|
||||
return (
|
||||
<Albums
|
||||
albums={albums.map(album => ({
|
||||
id: album.id,
|
||||
title: album.title,
|
||||
artist: album.artist,
|
||||
cover: album.cover,
|
||||
}))}
|
||||
onAlbumPress={() => {}}
|
||||
onSeeAll={() => {}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AlbumsWithData;
|
||||
25
mobile/src/Components/Artists/ArtistsWithData.tsx
Normal file
25
mobile/src/Components/Artists/ArtistsWithData.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React, {FC} from 'react';
|
||||
import Artists from './Artists';
|
||||
import {useGetArtistsQuery} from '../../Hooks/GraphQL';
|
||||
|
||||
const ArtistsWithData: FC = () => {
|
||||
const {data, loading} = useGetArtistsQuery({
|
||||
variables: {
|
||||
limit: 10,
|
||||
},
|
||||
});
|
||||
const artists = !loading && data ? data.artists : [];
|
||||
return (
|
||||
<Artists
|
||||
artists={artists.map(artist => ({
|
||||
id: artist.id,
|
||||
name: artist.name,
|
||||
cover: artist.picture,
|
||||
}))}
|
||||
onSeeAll={() => {}}
|
||||
onArtistPress={() => {}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ArtistsWithData;
|
||||
151
mobile/src/Components/MiniPlayer/MiniPlayer.tsx
Normal file
151
mobile/src/Components/MiniPlayer/MiniPlayer.tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
import styled from '@emotion/native';
|
||||
import React, {FC, useState, useEffect} from 'react';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import {Keyboard} from 'react-native';
|
||||
import {useCover} from '../../Hooks/useCover';
|
||||
|
||||
const Container = styled.TouchableOpacity`
|
||||
flex-direction: row;
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
background-color: #000;
|
||||
`;
|
||||
|
||||
const TrackTitle = styled.Text`
|
||||
color: #fff;
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 16px;
|
||||
`;
|
||||
|
||||
const TrackArtist = styled.Text`
|
||||
color: #a7a7a9;
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
`;
|
||||
|
||||
const TrackInfo = styled.View`
|
||||
flex-direction: column;
|
||||
margin-left: 20px;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const Cover = styled.Image`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 2px;
|
||||
`;
|
||||
|
||||
const NoCover = styled.View`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background-color: #161515;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 3px;
|
||||
`;
|
||||
|
||||
const ProgressbarContainer = styled.View`
|
||||
width: 100%;
|
||||
height: 1.5px;
|
||||
background-color: #4f4f4f;
|
||||
`;
|
||||
|
||||
const ProgressbarFill = styled.View<{progress: number}>`
|
||||
width: 0%;
|
||||
height: 100%;
|
||||
background-color: #ab28fc;
|
||||
${({progress}) => `width: ${progress}%`};
|
||||
`;
|
||||
|
||||
const Button = styled.TouchableOpacity`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export type ProgressbarProps = {
|
||||
progress: number;
|
||||
};
|
||||
const Progressbar: FC<ProgressbarProps> = props => {
|
||||
return (
|
||||
<ProgressbarContainer>
|
||||
<ProgressbarFill {...props} />
|
||||
</ProgressbarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export type MiniPlayerProps = {
|
||||
track: any;
|
||||
playing: boolean;
|
||||
progress: number;
|
||||
onPlay: () => void;
|
||||
onPause: () => void;
|
||||
onSkipNext: () => void;
|
||||
onOpenPlayer: () => void;
|
||||
};
|
||||
|
||||
const MiniPlayer: FC<MiniPlayerProps> = props => {
|
||||
const {track, playing, progress, onPlay, onPause, onSkipNext, onOpenPlayer} =
|
||||
props;
|
||||
const [keyboardVisible, setKeyboardVisible] = useState(false);
|
||||
const cover = useCover(track.cover);
|
||||
|
||||
// listen to keyboard events
|
||||
useEffect(() => {
|
||||
const subscription = Keyboard.addListener('keyboardDidShow', () => {
|
||||
setKeyboardVisible(true);
|
||||
});
|
||||
const subscription2 = Keyboard.addListener('keyboardDidHide', () => {
|
||||
setKeyboardVisible(false);
|
||||
});
|
||||
return () => {
|
||||
subscription.remove();
|
||||
subscription2.remove();
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (keyboardVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Progressbar progress={progress} />
|
||||
<Container onPress={onOpenPlayer}>
|
||||
<>
|
||||
{!track.cover && (
|
||||
<NoCover>
|
||||
<Feather name="music" size={30} color="#a7a7a9" />
|
||||
</NoCover>
|
||||
)}
|
||||
{track.cover && <Cover source={{uri: cover}} />}
|
||||
</>
|
||||
<TrackInfo>
|
||||
<TrackTitle numberOfLines={1}>{track.title}</TrackTitle>
|
||||
<TrackArtist numberOfLines={1}>{track.artist}</TrackArtist>
|
||||
</TrackInfo>
|
||||
<>
|
||||
{!playing && (
|
||||
<Button onPress={onPlay}>
|
||||
<Ionicons name="play" size={24} color="#fff" />
|
||||
</Button>
|
||||
)}
|
||||
{playing && (
|
||||
<Button onPress={onPause}>
|
||||
<Ionicons name="pause" size={24} color="#fff" />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
<Button onPress={onSkipNext}>
|
||||
<Ionicons name="play-skip-forward" size={24} color="#fff" />
|
||||
</Button>
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MiniPlayer;
|
||||
158
mobile/src/Components/Songs/Songs.tsx
Normal file
158
mobile/src/Components/Songs/Songs.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import styled from '@emotion/native';
|
||||
import React, {FC} from 'react';
|
||||
import {TouchableWithoutFeedback} from 'react-native';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import {Track as TrackType} from '../../Types';
|
||||
import {useCover} from '../../Hooks/useCover';
|
||||
|
||||
const Container = styled.View`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const Header = styled.View`
|
||||
margin: 0 20px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const Title = styled.Text`
|
||||
color: #fff;
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 24px;
|
||||
`;
|
||||
|
||||
const SeeAll = styled.Text`
|
||||
color: #a7a7a9;
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
const TrackRow = styled.View`
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: 20px;
|
||||
height: 60px;
|
||||
`;
|
||||
|
||||
const TrackTitle = styled.Text<{active?: boolean}>`
|
||||
color: #fff;
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 16px;
|
||||
${props => props.active && 'color: #ab28fc;'}
|
||||
`;
|
||||
|
||||
const TrackArtist = styled.Text`
|
||||
color: #a7a7a9;
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
`;
|
||||
|
||||
const TrackInfo = styled.View`
|
||||
flex-direction: column;
|
||||
margin-left: 20px;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const Cover = styled.Image`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 3px;
|
||||
`;
|
||||
|
||||
const NoCover = styled.View`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background-color: #161515;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 3px;
|
||||
`;
|
||||
|
||||
const TrackWrapper = styled.View`
|
||||
background-color: #232323;
|
||||
`;
|
||||
|
||||
const TouchableTrack = styled.TouchableOpacity`
|
||||
background-color: #000;
|
||||
justify-content: center;
|
||||
height: 70px;
|
||||
`;
|
||||
|
||||
const Button = styled.TouchableOpacity`
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
export type TrackProps = {
|
||||
item: any;
|
||||
active: boolean;
|
||||
onPress: (item: any) => void;
|
||||
};
|
||||
|
||||
const Track: FC<TrackProps> = props => {
|
||||
const {item, active, onPress} = props;
|
||||
const cover = useCover(item.cover);
|
||||
return (
|
||||
<TrackWrapper>
|
||||
<TouchableTrack onPress={() => onPress(item)}>
|
||||
<TrackRow>
|
||||
{item.cover && <Cover source={{uri: cover}} />}
|
||||
{!item.cover && (
|
||||
<NoCover>
|
||||
<Feather name="music" size={30} color="#a7a7a9" />
|
||||
</NoCover>
|
||||
)}
|
||||
<TrackInfo>
|
||||
<TrackTitle active={active} ellipsizeMode="tail" numberOfLines={1}>
|
||||
{item.title}
|
||||
</TrackTitle>
|
||||
<TrackArtist ellipsizeMode="tail" numberOfLines={1}>
|
||||
{item.artist}
|
||||
</TrackArtist>
|
||||
</TrackInfo>
|
||||
<Button>
|
||||
<Ionicons name="ellipsis-vertical" color={'#ffffff99'} size={18} />
|
||||
</Button>
|
||||
</TrackRow>
|
||||
</TouchableTrack>
|
||||
</TrackWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export type SongsProps = {
|
||||
tracks: TrackType[];
|
||||
currentTrack?: TrackType;
|
||||
onSeeAll: () => void;
|
||||
onPressTrack: (item: any) => void;
|
||||
};
|
||||
|
||||
const Songs: FC<SongsProps> = props => {
|
||||
const {tracks, currentTrack, onSeeAll, onPressTrack} = props;
|
||||
return (
|
||||
<Container>
|
||||
<Header>
|
||||
<Title>Tracks</Title>
|
||||
<TouchableWithoutFeedback onPress={onSeeAll}>
|
||||
<SeeAll>See All</SeeAll>
|
||||
</TouchableWithoutFeedback>
|
||||
</Header>
|
||||
{tracks.map((item: any) => (
|
||||
<Track
|
||||
key={item.id}
|
||||
item={item}
|
||||
active={item.id === currentTrack?.id}
|
||||
onPress={onPressTrack}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Songs;
|
||||
34
mobile/src/Components/Songs/SongsWithData.tsx
Normal file
34
mobile/src/Components/Songs/SongsWithData.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React, {FC} from 'react';
|
||||
import Songs from './Songs';
|
||||
import {useRecoilState} from 'recoil';
|
||||
import {currentTrackState} from '../CurrentTrack/CurrentTrackState';
|
||||
import {useGetTracksQuery} from '../../Hooks/GraphQL';
|
||||
|
||||
const SongsWithData: FC = () => {
|
||||
const {data, loading} = useGetTracksQuery({
|
||||
variables: {
|
||||
limit: 10,
|
||||
},
|
||||
});
|
||||
const tracks = !loading && data ? data.tracks : [];
|
||||
const [currentTrack, setCurrentTrack] = useRecoilState(currentTrackState);
|
||||
return (
|
||||
<Songs
|
||||
tracks={tracks.map(track => ({
|
||||
id: track.id,
|
||||
title: track.title,
|
||||
artist: track.artist,
|
||||
album: track.album.title,
|
||||
duration: track.duration!,
|
||||
cover: track.album.cover || undefined,
|
||||
artistId: track.artists[0].id,
|
||||
albumId: track.album.id,
|
||||
}))}
|
||||
currentTrack={currentTrack}
|
||||
onPressTrack={setCurrentTrack}
|
||||
onSeeAll={() => {}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default SongsWithData;
|
||||
108
mobile/src/Components/TrackRow/TrackRow.tsx
Normal file
108
mobile/src/Components/TrackRow/TrackRow.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import React, {FC} from 'react';
|
||||
import {Track} from '../../Types';
|
||||
import styled, {css} from '@emotion/native';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import {useCover} from '../../Hooks/useCover';
|
||||
|
||||
const Container = styled.View`
|
||||
height: 80px;
|
||||
width: 100%;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding-left: 20px;
|
||||
`;
|
||||
|
||||
const Cover = styled.Image`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
`;
|
||||
|
||||
const NoAlbumCover = styled.View`
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background-color: #161515;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const Title = styled.Text<{active: boolean}>`
|
||||
color: #fff;
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 16px;
|
||||
${props =>
|
||||
props.active &&
|
||||
css`
|
||||
color: #ab28fc;
|
||||
`}
|
||||
`;
|
||||
|
||||
const Artist = styled.Text`
|
||||
color: #a7a7a9;
|
||||
font-family: 'Gilroy-Bold';
|
||||
font-size: 14px;
|
||||
margin-top: 2px;
|
||||
`;
|
||||
|
||||
const AlbumInfo = styled.View`
|
||||
flex-direction: column;
|
||||
margin-left: 15px;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const Button = styled.TouchableOpacity`
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const TrackWrapper = styled.View`
|
||||
background-color: #232323;
|
||||
`;
|
||||
|
||||
const TouchableTrack = styled.TouchableOpacity`
|
||||
background-color: #000;
|
||||
`;
|
||||
|
||||
export type TrackRowProps = {
|
||||
track: Track;
|
||||
currentTrack?: Track;
|
||||
onPlay: (item: Track) => void;
|
||||
};
|
||||
|
||||
const TrackRow: FC<TrackRowProps> = props => {
|
||||
const {track, currentTrack, onPlay} = props;
|
||||
const cover = useCover(track.cover);
|
||||
return (
|
||||
<TrackWrapper>
|
||||
<TouchableTrack onPress={() => onPlay(track)}>
|
||||
<Container>
|
||||
{track.cover && <Cover source={{uri: cover}} />}
|
||||
{!track.cover && (
|
||||
<NoAlbumCover>
|
||||
<Feather name="disc" size={40} color="#a7a7a9" />
|
||||
</NoAlbumCover>
|
||||
)}
|
||||
<AlbumInfo>
|
||||
<Title
|
||||
numberOfLines={1}
|
||||
ellipsizeMode="tail"
|
||||
active={track.id === currentTrack?.id}>
|
||||
{track.title}
|
||||
</Title>
|
||||
<Artist numberOfLines={1} ellipsizeMode="tail">
|
||||
{track.artist}
|
||||
</Artist>
|
||||
</AlbumInfo>
|
||||
<Button>
|
||||
<Ionicons name="ellipsis-vertical" color={'#ffffff99'} size={18} />
|
||||
</Button>
|
||||
</Container>
|
||||
</TouchableTrack>
|
||||
</TrackWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default TrackRow;
|
||||
7
mobile/src/Hooks/useCover.tsx
Normal file
7
mobile/src/Hooks/useCover.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import Config from 'react-native-config';
|
||||
|
||||
export const useCover = (cover?: string) => {
|
||||
return cover?.startsWith('http')
|
||||
? cover
|
||||
: `${Config.API_URL?.replace('/graphql', '/covers')}/${cover}`;
|
||||
};
|
||||
15
mobile/src/Hooks/useFormat.tsx
Normal file
15
mobile/src/Hooks/useFormat.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
export const useTimeFormat = () => {
|
||||
const formatTime = (millis: number) => {
|
||||
let minutes = Math.floor(millis / 60000);
|
||||
const seconds = ((millis % 60000) / 1000).toFixed(0);
|
||||
const secondsDisplay = seconds.length === 1 ? `0${seconds}` : seconds;
|
||||
|
||||
if (seconds === '60') {
|
||||
minutes += 1;
|
||||
return `${minutes < 10 ? `0${minutes}` : minutes}:00`;
|
||||
} else {
|
||||
return `${minutes < 10 ? `0${minutes}` : minutes}:${secondsDisplay}`;
|
||||
}
|
||||
};
|
||||
return {formatTime};
|
||||
};
|
||||
@@ -1,6 +1,8 @@
|
||||
use anyhow::Error;
|
||||
use music_player_entity::{album as album_entity, artist as artist_entity, track as track_entity};
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter, QueryOrder};
|
||||
use sea_orm::{
|
||||
ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter, QueryOrder, QuerySelect,
|
||||
};
|
||||
|
||||
pub struct AlbumRepository {
|
||||
db: DatabaseConnection,
|
||||
@@ -38,28 +40,33 @@ impl AlbumRepository {
|
||||
pub async fn find_all(
|
||||
&self,
|
||||
filter: Option<String>,
|
||||
offset: Option<u64>,
|
||||
limit: Option<u64>,
|
||||
) -> Result<Vec<album_entity::Model>, Error> {
|
||||
let mut query = match offset {
|
||||
Some(offset) => album_entity::Entity::find()
|
||||
.order_by_asc(album_entity::Column::Title)
|
||||
.offset(offset),
|
||||
None => album_entity::Entity::find().order_by_asc(album_entity::Column::Title),
|
||||
};
|
||||
query = match limit {
|
||||
Some(limit) => query.limit(limit),
|
||||
None => query,
|
||||
};
|
||||
match filter {
|
||||
Some(filter) => {
|
||||
if filter.is_empty() {
|
||||
let results = album_entity::Entity::find()
|
||||
.order_by_asc(album_entity::Column::Title)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
let results = query.all(&self.db).await?;
|
||||
return Ok(results);
|
||||
}
|
||||
let results = album_entity::Entity::find()
|
||||
let results = query
|
||||
.filter(album_entity::Column::Title.like(format!("%{}%", filter).as_str()))
|
||||
.order_by_asc(album_entity::Column::Title)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(results)
|
||||
}
|
||||
None => {
|
||||
let results = album_entity::Entity::find()
|
||||
.order_by_asc(album_entity::Column::Title)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
let results = query.all(&self.db).await?;
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Error;
|
||||
use music_player_entity::{album as album_entity, artist as artist_entity, track as track_entity};
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder};
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder, QuerySelect};
|
||||
|
||||
pub struct ArtistRepository {
|
||||
db: DatabaseConnection,
|
||||
@@ -50,28 +50,35 @@ impl ArtistRepository {
|
||||
pub async fn find_all(
|
||||
&self,
|
||||
filter: Option<String>,
|
||||
offset: Option<u64>,
|
||||
limit: Option<u64>,
|
||||
) -> Result<Vec<artist_entity::Model>, Error> {
|
||||
let mut query = match offset {
|
||||
Some(offset) => artist_entity::Entity::find()
|
||||
.order_by_asc(artist_entity::Column::Name)
|
||||
.offset(offset),
|
||||
None => artist_entity::Entity::find().order_by_asc(artist_entity::Column::Name),
|
||||
};
|
||||
|
||||
query = match limit {
|
||||
Some(limit) => query.limit(limit),
|
||||
None => query,
|
||||
};
|
||||
|
||||
match filter {
|
||||
Some(filter) => {
|
||||
if filter.is_empty() {
|
||||
let results = artist_entity::Entity::find()
|
||||
.order_by_asc(artist_entity::Column::Name)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
let results = query.all(&self.db).await?;
|
||||
return Ok(results);
|
||||
}
|
||||
let results = artist_entity::Entity::find()
|
||||
let results = query
|
||||
.filter(artist_entity::Column::Name.like(format!("%{}%", filter).as_str()))
|
||||
.order_by_asc(artist_entity::Column::Name)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
Ok(results)
|
||||
}
|
||||
None => {
|
||||
let results = artist_entity::Entity::find()
|
||||
.order_by_asc(artist_entity::Column::Name)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
let results = query.all(&self.db).await?;
|
||||
Ok(results)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,21 +42,24 @@ impl TrackRepository {
|
||||
pub async fn find_all(
|
||||
&self,
|
||||
filter: Option<String>,
|
||||
offset: Option<u64>,
|
||||
limit: u64,
|
||||
) -> Result<Vec<track_entity::Model>, Error> {
|
||||
let query = match offset {
|
||||
Some(offset) => track_entity::Entity::find().offset(offset).limit(limit),
|
||||
None => track_entity::Entity::find().limit(limit),
|
||||
};
|
||||
let results = match filter {
|
||||
Some(filter) => {
|
||||
if filter.is_empty() {
|
||||
track_entity::Entity::find()
|
||||
.limit(limit)
|
||||
query
|
||||
.order_by_asc(track_entity::Column::Title)
|
||||
.find_with_related(artist_entity::Entity)
|
||||
.all(&self.db)
|
||||
.await?
|
||||
} else {
|
||||
track_entity::Entity::find()
|
||||
query
|
||||
.filter(track_entity::Column::Title.like(format!("%{}%", filter).as_str()))
|
||||
.limit(limit)
|
||||
.order_by_asc(track_entity::Column::Title)
|
||||
.find_with_related(artist_entity::Entity)
|
||||
.all(&self.db)
|
||||
@@ -64,8 +67,7 @@ impl TrackRepository {
|
||||
}
|
||||
}
|
||||
None => {
|
||||
track_entity::Entity::find()
|
||||
.limit(limit)
|
||||
query
|
||||
.order_by_asc(track_entity::Column::Title)
|
||||
.find_with_related(artist_entity::Entity)
|
||||
.all(&self.db)
|
||||
@@ -73,13 +75,16 @@ impl TrackRepository {
|
||||
}
|
||||
};
|
||||
|
||||
let albums: Vec<(track_entity::Model, Option<album_entity::Model>)> =
|
||||
track_entity::Entity::find()
|
||||
.limit(limit)
|
||||
.order_by_asc(track_entity::Column::Title)
|
||||
.find_also_related(album_entity::Entity)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
let query = match offset {
|
||||
Some(offset) => track_entity::Entity::find().offset(offset).limit(limit),
|
||||
None => track_entity::Entity::find().limit(limit),
|
||||
};
|
||||
|
||||
let albums: Vec<(track_entity::Model, Option<album_entity::Model>)> = query
|
||||
.order_by_asc(track_entity::Column::Title)
|
||||
.find_also_related(album_entity::Entity)
|
||||
.all(&self.db)
|
||||
.await?;
|
||||
|
||||
let albums: Vec<Option<album_entity::Model>> = albums
|
||||
.into_iter()
|
||||
|
||||
Reference in New Issue
Block a user