Merge branch 'main' into feat/adc/search-history-V2
This commit is contained in:
@@ -3,62 +3,42 @@ import { View } from 'react-native';
|
||||
import { useBreakpointValue } from 'native-base';
|
||||
import HomeMainSongCard from './HomeMainSongCard';
|
||||
import GoldenRatioPanel from './GoldenRatioPanel';
|
||||
import Song from '../../models/Song';
|
||||
import { useNavigation } from '../../Navigation';
|
||||
|
||||
type HomeCardProps = {
|
||||
image: string;
|
||||
title: string;
|
||||
artist: string;
|
||||
fontSize: number;
|
||||
onPress?: () => void;
|
||||
type GoldenRatioProps = {
|
||||
songs: Song[];
|
||||
};
|
||||
|
||||
const cards = [
|
||||
{
|
||||
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688155292180560/image_homeview1.png',
|
||||
title: 'Beethoven',
|
||||
artist: 'Synphony No. 9',
|
||||
fontSize: 46,
|
||||
},
|
||||
{
|
||||
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688154923090093/image_homeview2.png',
|
||||
title: 'Mozart',
|
||||
artist: 'Lieder Kantate KV 619',
|
||||
fontSize: 36,
|
||||
},
|
||||
{
|
||||
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688154499457096/image_homeview3.png',
|
||||
title: 'Back',
|
||||
artist: 'Truc Truc',
|
||||
fontSize: 26,
|
||||
},
|
||||
{
|
||||
image: 'https://media.discordapp.net/attachments/717080637038788731/1153688154109394985/image_homeview4.png',
|
||||
title: 'Mozart',
|
||||
artist: 'Machin Machin',
|
||||
fontSize: 22,
|
||||
},
|
||||
] as [HomeCardProps, HomeCardProps, HomeCardProps, HomeCardProps];
|
||||
|
||||
const GoldenRatio = () => {
|
||||
const GoldenRatio = (props: GoldenRatioProps) => {
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isPhone = screenSize === 'small';
|
||||
const navigation = useNavigation();
|
||||
const fontSizes = [46, 36, 26, 22];
|
||||
const cards = props.songs.map((s, i) => ({
|
||||
image: s.cover,
|
||||
title: s.name,
|
||||
artist: s.artist?.name ?? '',
|
||||
fontSize: fontSizes.at(i) ?? fontSizes.at(-1)!,
|
||||
onPress: () => navigation.navigate('Play', { songId: s.id }),
|
||||
}));
|
||||
|
||||
return (
|
||||
<GoldenRatioPanel
|
||||
direction={isPhone ? 'column' : 'row'}
|
||||
header={<HomeMainSongCard {...cards[0]} />}
|
||||
header={<HomeMainSongCard {...cards[0]!} />}
|
||||
>
|
||||
<GoldenRatioPanel
|
||||
direction={isPhone ? 'row' : 'column'}
|
||||
header={<HomeMainSongCard {...cards[1]} />}
|
||||
header={<HomeMainSongCard {...cards[1]!} />}
|
||||
>
|
||||
<GoldenRatioPanel
|
||||
direction={isPhone ? 'column-reverse' : 'row-reverse'}
|
||||
header={<HomeMainSongCard {...cards[2]} />}
|
||||
header={<HomeMainSongCard {...cards[2]!} />}
|
||||
>
|
||||
<GoldenRatioPanel
|
||||
direction={isPhone ? 'row-reverse' : 'column-reverse'}
|
||||
header={<HomeMainSongCard {...cards[3]} />}
|
||||
header={<HomeMainSongCard numLinesHeader={1} {...cards[3]!} />}
|
||||
>
|
||||
<View style={{ display: 'flex', width: '100%', height: '100%' }}></View>
|
||||
</GoldenRatioPanel>
|
||||
|
||||
@@ -6,6 +6,7 @@ type HomeMainSongCardProps = {
|
||||
title: string;
|
||||
artist: string;
|
||||
fontSize: number;
|
||||
numLinesHeader: number;
|
||||
onPress: () => void;
|
||||
};
|
||||
|
||||
@@ -66,7 +67,7 @@ const HomeMainSongCard = (props: HomeMainSongCardProps) => {
|
||||
fontSize: props.fontSize,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
numberOfLines={2}
|
||||
numberOfLines={props.numLinesHeader}
|
||||
selectable={false}
|
||||
>
|
||||
{props.title}
|
||||
@@ -94,6 +95,7 @@ const HomeMainSongCard = (props: HomeMainSongCardProps) => {
|
||||
HomeMainSongCard.defaultProps = {
|
||||
onPress: () => {},
|
||||
fontSize: 16,
|
||||
numLinesHeader: 2,
|
||||
};
|
||||
|
||||
export default HomeMainSongCard;
|
||||
|
||||
@@ -140,18 +140,16 @@ const SearchBarComponent = () => {
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
{!artist
|
||||
? artistsQuery.data?.map((artist, index) => (
|
||||
<ArtistChipComponent
|
||||
key={index}
|
||||
name={artist.name}
|
||||
onPress={() => {
|
||||
setArtist(artist.name);
|
||||
}}
|
||||
/>
|
||||
// eslint-disable-next-line no-mixed-spaces-and-tabs
|
||||
))
|
||||
: null}
|
||||
{!artist &&
|
||||
artistsQuery.data?.map((artist, index) => (
|
||||
<ArtistChipComponent
|
||||
key={index}
|
||||
name={artist.name}
|
||||
onPress={() => {
|
||||
setArtist(artist.name);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
<View>
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import Song from '../../models/Song';
|
||||
import React from 'react';
|
||||
import { Image, View } from 'react-native';
|
||||
import { LikeButton } from './SongCardInfoLikeBtn';
|
||||
import { Image, Platform, View } from 'react-native';
|
||||
import { Pressable, Text, PresenceTransition, Icon, useBreakpointValue } from 'native-base';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useQuery } from '../../Queries';
|
||||
import API from '../../API';
|
||||
import { useLikeSongMutation } from '../../utils/likeSongMutation';
|
||||
import Hoverable from '../Hoverable';
|
||||
|
||||
type SongCardInfoProps = {
|
||||
song: Song;
|
||||
@@ -10,35 +15,45 @@ type SongCardInfoProps = {
|
||||
onPlay: () => void;
|
||||
};
|
||||
|
||||
const Scores = [
|
||||
{
|
||||
icon: 'warning',
|
||||
score: 3,
|
||||
},
|
||||
{
|
||||
icon: 'star',
|
||||
score: -225,
|
||||
},
|
||||
{
|
||||
icon: 'trophy',
|
||||
score: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const SongCardInfo = (props: SongCardInfoProps) => {
|
||||
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
|
||||
const isPhone = screenSize === 'small';
|
||||
const [isPlayHovered, setIsPlayHovered] = React.useState(false);
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
const [isSlided, setIsSlided] = React.useState(false);
|
||||
const user = useQuery(API.getUserInfo);
|
||||
const [isLiked, setIsLiked] = React.useState(false);
|
||||
const { mutate } = useLikeSongMutation();
|
||||
|
||||
const CardDims = {
|
||||
height: isPhone ? 160 : 200,
|
||||
width: isPhone ? 160 : 200,
|
||||
};
|
||||
|
||||
const Scores = [
|
||||
{
|
||||
icon: 'time',
|
||||
score: props.song.lastScore ?? '-',
|
||||
},
|
||||
{
|
||||
icon: 'trophy',
|
||||
score: props.song.bestScore ?? '-',
|
||||
},
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!user.data) {
|
||||
return;
|
||||
}
|
||||
setIsLiked(
|
||||
props.song.likedByUsers?.some(({ userId }) => userId === user.data?.id) ?? false
|
||||
);
|
||||
}, [user.data, props.song.likedByUsers]);
|
||||
|
||||
return (
|
||||
<View
|
||||
<Pressable
|
||||
delayHoverIn={7}
|
||||
onPress={Platform.OS === 'android' ? props.onPress : undefined}
|
||||
style={{
|
||||
width: CardDims.width,
|
||||
height: CardDims.height,
|
||||
@@ -48,227 +63,201 @@ const SongCardInfo = (props: SongCardInfoProps) => {
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.70)',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
}}
|
||||
onHoverIn={() => {
|
||||
setIsHovered(true);
|
||||
}}
|
||||
onHoverOut={() => {
|
||||
setIsHovered(false);
|
||||
setIsSlided(false);
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
delayHoverIn={7}
|
||||
isHovered={isPlayHovered ? true : undefined}
|
||||
onPress={props.onPress}
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
}}
|
||||
onHoverIn={() => {
|
||||
setIsHovered(true);
|
||||
}}
|
||||
onHoverOut={() => {
|
||||
setIsHovered(false);
|
||||
setIsSlided(false);
|
||||
width: CardDims.width,
|
||||
height: CardDims.height,
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.7)',
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<View
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
marginBottom: 8,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{Scores.map((score, idx) => (
|
||||
<View
|
||||
key={score.icon + idx}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
paddingHorizontal: 10,
|
||||
}}
|
||||
>
|
||||
<Icon as={Ionicons} name={score.icon} size={17} color="white" />
|
||||
<Text
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{score.score}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<PresenceTransition
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
}}
|
||||
visible={isHovered}
|
||||
animate={{
|
||||
translateY: -55,
|
||||
}}
|
||||
onTransitionComplete={() => {
|
||||
if (isHovered) {
|
||||
setIsSlided(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: CardDims.width,
|
||||
height: CardDims.height,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: props.song.cover }}
|
||||
style={{
|
||||
width: CardDims.width,
|
||||
height: CardDims.height,
|
||||
backgroundColor: 'rgba(16, 16, 20, 0.7)',
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
marginBottom: 8,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
{Scores.map((score, idx) => (
|
||||
<View
|
||||
key={score.icon + idx}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
paddingHorizontal: 10,
|
||||
}}
|
||||
>
|
||||
<Icon as={Ionicons} name={score.icon} size={17} color="white" />
|
||||
<Text
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{score.score}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<PresenceTransition
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
}}
|
||||
visible={isHovered}
|
||||
initial={{
|
||||
translateY: 0,
|
||||
}}
|
||||
animate={{
|
||||
translateY: -55,
|
||||
}}
|
||||
onTransitionComplete={() => {
|
||||
if (isHovered) {
|
||||
setIsSlided(true);
|
||||
}
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 7,
|
||||
borderRadius: 12,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={{ uri: props.song.cover }}
|
||||
style={{
|
||||
position: 'relative',
|
||||
width: CardDims.width,
|
||||
height: CardDims.height,
|
||||
borderRadius: 12,
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'flex-start',
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 7,
|
||||
borderRadius: 12,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-end',
|
||||
justifyContent: 'space-between',
|
||||
flexShrink: 1,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={{
|
||||
flexShrink: 1,
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 14,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
{props.song.name}
|
||||
</Text>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
fontWeight: 'normal',
|
||||
}}
|
||||
>
|
||||
{props.song.artist?.name}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons
|
||||
{props.song.name}
|
||||
</Text>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
color: 'white',
|
||||
fontSize: 12,
|
||||
fontWeight: 'normal',
|
||||
}}
|
||||
name="bookmark-outline"
|
||||
size={17}
|
||||
color="#6075F9"
|
||||
/>
|
||||
>
|
||||
{props.song.artist?.name}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<LikeButton
|
||||
color="#6075F9"
|
||||
onPress={() => {
|
||||
console.log('like');
|
||||
mutate({ songId: props.song.id, like: !isLiked });
|
||||
}}
|
||||
isLiked={isLiked}
|
||||
/>
|
||||
</View>
|
||||
</PresenceTransition>
|
||||
<PresenceTransition
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
</View>
|
||||
</View>
|
||||
</PresenceTransition>
|
||||
{Platform.OS === 'web' && (
|
||||
<PresenceTransition
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 35,
|
||||
left: CardDims.width / 2 - 20,
|
||||
}}
|
||||
visible={isSlided}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
>
|
||||
<Hoverable
|
||||
onHoverIn={() => {
|
||||
setIsPlayHovered(true);
|
||||
}}
|
||||
visible={isSlided}
|
||||
initial={{
|
||||
opacity: 0,
|
||||
}}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
onHoverOut={() => {
|
||||
setIsPlayHovered(false);
|
||||
}}
|
||||
>
|
||||
<View
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onPress();
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: isPlayHovered
|
||||
? 'rgba(96, 117, 249, 0.9)'
|
||||
: 'rgba(96, 117, 249, 0.7)',
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onHoverIn={() => {
|
||||
setIsPlayHovered(true);
|
||||
}}
|
||||
onHoverOut={() => {
|
||||
setIsPlayHovered(false);
|
||||
}}
|
||||
borderRadius={100}
|
||||
marginBottom={35}
|
||||
onPress={props.onPlay}
|
||||
>
|
||||
{({ isPressed, isHovered }) => (
|
||||
<View
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 100,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: (() => {
|
||||
if (isPressed) {
|
||||
return 'rgba(96, 117, 249, 1)';
|
||||
} else if (isHovered) {
|
||||
return 'rgba(96, 117, 249, 0.9)';
|
||||
} else {
|
||||
return 'rgba(96, 117, 249, 0.7)';
|
||||
}
|
||||
})(),
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name="play-outline"
|
||||
color={'white'}
|
||||
size={20}
|
||||
rounded="sm"
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</Pressable>
|
||||
<Ionicons name="play-outline" color={'white'} size={20} rounded="sm" />
|
||||
</View>
|
||||
</PresenceTransition>
|
||||
</>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Hoverable>
|
||||
</PresenceTransition>
|
||||
)}
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Platform, View } from 'react-native';
|
||||
import { IconButton } from 'native-base';
|
||||
import { MaterialIcons } from '@expo/vector-icons';
|
||||
|
||||
type LikeButtonProps = {
|
||||
isLiked: boolean;
|
||||
onPress?: () => void;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export const LikeButton = ({ isLiked, color, onPress }: LikeButtonProps) => {
|
||||
if (Platform.OS === 'web') {
|
||||
// painful error of no onHover event control
|
||||
return (
|
||||
<View onClick={onPress}>
|
||||
<MaterialIcons
|
||||
color={color}
|
||||
name={isLiked ? 'favorite' : 'favorite-outline'}
|
||||
size={17}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<IconButton
|
||||
variant={'ghost'}
|
||||
borderRadius={'full'}
|
||||
size={17}
|
||||
color={color}
|
||||
onPress={onPress}
|
||||
_icon={{
|
||||
as: MaterialIcons,
|
||||
name: isLiked ? 'favorite' : 'favorite-outline',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user