feat: song illustrations code

This commit is contained in:
GitBluub
2023-05-30 03:19:51 +09:00
committed by Clément Le Bihan
parent 41d020e7a2
commit 6dcda01f6f
6 changed files with 254 additions and 159 deletions

View File

@@ -22,7 +22,7 @@ import { CreateSongDto } from './dto/create-song.dto';
import { SongService } from './song.service';
import { Request } from 'express';
import { Prisma, Song } from '@prisma/client';
import { createReadStream } from 'fs';
import { createReadStream, existsSync } from 'fs';
import { ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger';
import { HistoryService } from 'src/history/history.service';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
@@ -54,6 +54,9 @@ export class SongController {
if (!song) throw new NotFoundException('Song not found');
if (song.illustrationPath === null) throw new NotFoundException();
if (!existsSync(song.illustrationPath))
throw new NotFoundException('Illustration not found');
try {
const file = createReadStream(song.illustrationPath);
return new StreamableFile(file);

View File

@@ -46,22 +46,6 @@ export class APIError extends Error {
}
}
const dummyIllustrations = [
"https://i.discogs.com/syRCX8NaLwK2SMk8X6TVU_DWc8RRqE4b-tebAQ6kVH4/rs:fit/g:sm/q:90/h:600/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9SLTgyNTQz/OC0xNjE3ODE0NDI2/LTU1MjUuanBlZw.jpeg",
"https://folkr.fr/wp-content/uploads/2017/06/dua-lipa-folkr-2017-cover-04.jpg",
"https://folkr.fr/wp-content/uploads/2017/06/dua-lipa-folkr-2017-cover-03.jpg",
"https://cdn.fireemblemwiki.org/e/eb/Album_106698A.jpg",
"https://upload.wikimedia.org/wikipedia/en/8/84/California_Gurls_cover.png",
"https://upload.wikimedia.org/wikipedia/en/a/a2/Katy_Perry_-_Peacock_%282012%29.jpg",
"https://upload.wikimedia.org/wikipedia/en/d/d5/Katy_Perry_feat._Riff_Raff_-_This_Is_How_We_Do.png",
"https://upload.wikimedia.org/wikipedia/en/8/83/David_Guetta_-_I_Can_Only_Imagine.jpg",
"https://upload.wikimedia.org/wikipedia/en/f/f3/David_Guetta_-_Pop_Life_-_2007.jpg",
"https://upload.wikimedia.org/wikipedia/en/b/ba/David_Guetta_2U.jpg",
];
const getDummyIllustration = () =>
dummyIllustrations[Math.floor(Math.random() * dummyIllustrations.length)];
// we will need the same thing for the scorometer API url
const baseAPIUrl =
process.env.NODE_ENV != "development" && Platform.OS === "web"
@@ -244,6 +228,14 @@ export default class API {
return "11111";
}
/**
* Retrive a song's midi partition
* @param songId the id to find the song
*/
public static getSongIllustration(songId: number): string {
return `${baseAPIUrl}/song/${songId}/illustration`;
}
public static async getAllSongs(): Promise<Song[]> {
let songs = await API.fetch({
route: "/song",
@@ -259,7 +251,7 @@ export default class API {
albumId: song.albumId as number,
genreId: song.genreId as number,
details: song.difficulties,
cover: getDummyIllustration(),
cover: `${baseAPIUrl}/song/${song.id}/illustration`,
metrics: {},
} as Song)
);
@@ -282,7 +274,7 @@ export default class API {
albumId: song.albumId as number,
genreId: song.genreId as number,
details: song.difficulties,
cover: getDummyIllustration(),
cover: `${baseAPIUrl}/song/${song.id}/illustration`,
} as Song;
}
/**

View File

@@ -82,7 +82,7 @@ const SongRow = ({ song, onPress }: SongRowProps) => {
flexGrow={0}
pl={10}
style={{ zIndex: 0, aspectRatio: 1, borderRadius: 5 }}
source={{ uri: song.cover ?? "https://picsum.photos/200" }}
source={{ uri: API.getSongIllustration(song.id) }}
alt={song.name}
/>
<HStack

View File

@@ -1,39 +1,34 @@
import React from "react";
import Card, { CardBorderRadius } from './Card';
import { VStack, Text, Image } from 'native-base';
import Card, { CardBorderRadius } from "./Card";
import { VStack, Text, Image } from "native-base";
import { useNavigation } from "../Navigation";
import API from "../API";
type SongCardProps = {
cover: string;
name: string;
artistName: string;
songId: number
}
};
const SongCard = (props: SongCardProps) => {
const { cover, name, artistName, songId } = props;
const navigation = useNavigation();
return (
<Card
shadow={3}
onPress={() => navigation.navigate('Song', { songId })}
>
<Card shadow={3} onPress={() => navigation.navigate("Song", { songId })}>
<VStack m={1.5} space={3}>
<Image
style={{ zIndex: 0, aspectRatio: 1, borderRadius: CardBorderRadius}}
style={{ zIndex: 0, aspectRatio: 1, borderRadius: CardBorderRadius }}
source={{ uri: cover }}
alt={[props.name, props.artistName].join('-')}
alt={[props.name, props.artistName].join("-")}
/>
<VStack>
<Text isTruncated bold fontSize='md' noOfLines={2} height={50}>
<Text isTruncated bold fontSize="md" noOfLines={2} height={50}>
{name}
</Text>
<Text isTruncated >
{artistName}
</Text>
<Text isTruncated>{artistName}</Text>
</VStack>
</VStack>
</Card>
)
}
);
};
export default SongCard;
export default SongCard;

View File

@@ -1,10 +1,18 @@
import { Card, Column, Image, Row, Text, ScrollView, VStack } from "native-base"
import {
Card,
Column,
Image,
Row,
Text,
ScrollView,
VStack,
} from "native-base";
import Translate from "../components/Translate";
import SongCardGrid from "../components/SongCardGrid";
import { RouteProps, useNavigation } from "../Navigation";
import { CardBorderRadius } from "../components/Card";
import TextButton from "../components/TextButton";
import API from '../API';
import API from "../API";
import LoadingComponent from "../components/Loading";
import CardGridCustom from "../components/CardGridCustom";
import SongCard from "../components/SongCard";
@@ -12,52 +20,73 @@ import { useQueries, useQuery } from "react-query";
import { LoadingView } from "../components/Loading";
type ScoreViewProps = {
songId: number
overallScore: number,
songId: number;
overallScore: number;
score: {
missed: number,
good: number,
great: number,
perfect: number,
maxScore: number,
}
}
missed: number;
good: number;
great: number;
perfect: number;
maxScore: number;
};
};
const ScoreView = ({ songId, overallScore, score }: RouteProps<ScoreViewProps>) => {
const ScoreView = ({
songId,
overallScore,
score,
}: RouteProps<ScoreViewProps>) => {
const navigation = useNavigation();
const songQuery = useQuery(['song', songId], () => API.getSong(songId));
const artistQuery = useQuery(['song', songId, "artist"],
const songQuery = useQuery(["song", songId], () => API.getSong(songId));
const artistQuery = useQuery(
["song", songId, "artist"],
() => API.getArtist(songQuery.data!.artistId!),
{ enabled: songQuery.data != undefined }
);
// const perfoamnceRecommandationsQuery = useQuery(['song', props.songId, 'score', 'latest', 'recommendations'], () => API.getLastSongPerformanceScore(props.songId));
const recommendations = useQuery(['song', 'recommendations'], () => API.getSongSuggestions());
const artistRecommendations = useQueries(recommendations.data
?.filter(({ artistId }) => artistId !== null)
.map((song) => ({
queryKey: ['artist', song.artistId],
queryFn: () => API.getArtist(song.artistId!)
})) ?? []
)
const recommendations = useQuery(["song", "recommendations"], () =>
API.getSongSuggestions()
);
const artistRecommendations = useQueries(
recommendations.data
?.filter(({ artistId }) => artistId !== null)
.map((song) => ({
queryKey: ["artist", song.artistId],
queryFn: () => API.getArtist(song.artistId!),
})) ?? []
);
if (!recommendations.data || artistRecommendations.find(({ data }) => !data) || !songQuery.data || (songQuery.data.artistId && !artistQuery.data)) {
return <LoadingView/>;
if (
!recommendations.data ||
artistRecommendations.find(({ data }) => !data) ||
!songQuery.data ||
(songQuery.data.artistId && !artistQuery.data)
) {
return <LoadingView />;
}
return <ScrollView p={8} contentContainerStyle={{ alignItems: 'center' }}>
<VStack width={{ base: '100%', lg: '50%' }} textAlign='center'>
<Text bold fontSize='lg'>{songQuery.data.name}</Text>
<Text bold>{artistQuery.data?.name}</Text>
<Row style={{ justifyContent: 'center', display: 'flex' }}>
<Card shadow={3} style={{ flex: 1 }}>
<Image
style={{ zIndex: 0, aspectRatio: 1, margin: 5, borderRadius: CardBorderRadius}}
source={{ uri: songQuery.data.cover }}
/>
</Card>
<Card shadow={3} style={{ flex: 1 }}>
<Column style={{ justifyContent: 'space-evenly', flexGrow: 1 }}>
{/*<Row style={{ alignItems: 'center' }}>
return (
<ScrollView p={8} contentContainerStyle={{ alignItems: "center" }}>
<VStack width={{ base: "100%", lg: "50%" }} textAlign="center">
<Text bold fontSize="lg">
{songQuery.data.name}
</Text>
<Text bold>{artistQuery.data?.name}</Text>
<Row style={{ justifyContent: "center", display: "flex" }}>
<Card shadow={3} style={{ flex: 1 }}>
<Image
style={{
zIndex: 0,
aspectRatio: 1,
margin: 5,
borderRadius: CardBorderRadius,
}}
source={{ uri: songQuery.data.cover }}
/>
</Card>
<Card shadow={3} style={{ flex: 1 }}>
<Column style={{ justifyContent: "space-evenly", flexGrow: 1 }}>
{/*<Row style={{ alignItems: 'center' }}>
<Text bold fontSize='xl'>
</Text>
@@ -69,40 +98,46 @@ const ScoreView = ({ songId, overallScore, score }: RouteProps<ScoreViewProps>)
</Text>
<Translate translationKey='goodNotesInARow' format={(t) => ' ' + t}/>
</Row>*/}
<Row style={{ alignItems: 'center' }}>
<Translate translationKey='score' format={(t) => t + ' : '}/>
<Text bold fontSize='xl'>
{overallScore + "pts"}
</Text>
</Row>
</Column>
</Card>
</Row>
<CardGridCustom
style={{ justifyContent: "space-evenly" }}
content={recommendations.data.map((i) => ({
cover: i.cover,
name: i.name ,
artistName: artistRecommendations.find(({ data }) => data?.id == i.artistId)?.data?.name ?? "",
id: i.id
}))}
cardComponent={SongCard}
heading={<Text fontSize='sm'>
<Translate translationKey="songsToGetBetter"/>
</Text>}
/>
<Row space={3} style={{ width: '100%', justifyContent: 'center' }}>
<TextButton colorScheme='gray'
translate={{ translationKey: 'backBtn' }}
onPress={() => navigation.navigate('Home')}
<Row style={{ alignItems: "center" }}>
<Translate translationKey="score" format={(t) => t + " : "} />
<Text bold fontSize="xl">
{overallScore + "pts"}
</Text>
</Row>
</Column>
</Card>
</Row>
<CardGridCustom
style={{ justifyContent: "space-evenly" }}
content={recommendations.data.map((i) => ({
cover: i.cover,
name: i.name,
artistName:
artistRecommendations.find(({ data }) => data?.id == i.artistId)
?.data?.name ?? "",
id: i.id,
}))}
cardComponent={SongCard}
heading={
<Text fontSize="sm">
<Translate translationKey="songsToGetBetter" />
</Text>
}
/>
<TextButton
onPress={() => navigation.navigate('Song', { songId })}
translate={{ translationKey: 'playAgain' }}
/>
</Row>
</VStack>
</ScrollView>
}
<Row space={3} style={{ width: "100%", justifyContent: "center" }}>
<TextButton
colorScheme="gray"
translate={{ translationKey: "backBtn" }}
onPress={() => navigation.navigate("Home")}
/>
<TextButton
onPress={() => navigation.navigate("Song", { songId })}
translate={{ translationKey: "playAgain" }}
/>
</Row>
</VStack>
</ScrollView>
);
};
export default ScoreView;

View File

@@ -1,10 +1,20 @@
import { Divider, Box, Center, Image, Text, VStack, PresenceTransition, Icon, Stack } from "native-base";
import { useQuery } from 'react-query';
import {
Divider,
Box,
Center,
Image,
Text,
VStack,
PresenceTransition,
Icon,
Stack,
} from "native-base";
import { useQuery } from "react-query";
import LoadingComponent, { LoadingView } from "../components/Loading";
import React, { useEffect, useState } from "react";
import { Translate, translate } from "../i18n/i18n";
import formatDuration from "format-duration";
import { Ionicons } from '@expo/vector-icons';
import { Ionicons } from "@expo/vector-icons";
import API from "../API";
import TextButton from "../components/TextButton";
import { RouteProps, useNavigation } from "../Navigation";
@@ -16,84 +26,144 @@ interface SongLobbyProps {
const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
const navigation = useNavigation();
const songQuery = useQuery(['song', props.songId], () => API.getSong(props.songId));
const chaptersQuery = useQuery(['song', props.songId, 'chapters'], () => API.getSongChapters(props.songId));
const scoresQuery = useQuery(['song', props.songId, 'scores'], () => API.getSongHistory(props.songId));
const songQuery = useQuery(["song", props.songId], () =>
API.getSong(props.songId)
);
const chaptersQuery = useQuery(["song", props.songId, "chapters"], () =>
API.getSongChapters(props.songId)
);
const scoresQuery = useQuery(["song", props.songId, "scores"], () =>
API.getSongHistory(props.songId)
);
const [chaptersOpen, setChaptersOpen] = useState(false);
useEffect(() => {
if (chaptersOpen && !chaptersQuery.data)
chaptersQuery.refetch();
if (chaptersOpen && !chaptersQuery.data) chaptersQuery.refetch();
}, [chaptersOpen]);
useEffect(() => {}, [songQuery.isLoading]);
if (songQuery.isLoading || scoresQuery.isLoading)
return <LoadingView/>;
useEffect(() => { }, [songQuery.isLoading]);
if (songQuery.isLoading || scoresQuery.isLoading) return <LoadingView />;
return (
<Box style={{ padding: 30, flexDirection: 'column' }}>
<Box style={{ flexDirection: 'row', height: '30%'}}>
<Box style={{ padding: 30, flexDirection: "column" }}>
<Box style={{ flexDirection: "row", height: "30%" }}>
<Box style={{ flex: 3 }}>
<Image source={{ uri: songQuery.data!.cover }} alt={songQuery.data?.name} style={{ height: '100%', width: undefined, resizeMode: 'contain', aspectRatio: 1 }}/>
<Image
source={{ uri: songQuery.data!.cover }}
alt={songQuery.data?.name}
style={{
height: "100%",
width: undefined,
resizeMode: "contain",
aspectRatio: 1,
}}
/>
</Box>
<Box style={{ flex: 0.5 }}/>
<Box style={{ flex: 3, padding: 10, flexDirection: 'column', justifyContent: 'space-between' }}>
<Box style={{ flex: 0.5 }} />
<Box
style={{
flex: 3,
padding: 10,
flexDirection: "column",
justifyContent: "space-between",
}}
>
<Stack flex={1} space={3}>
<Text bold isTruncated numberOfLines={2} fontSize='lg'>{songQuery.data!.name}</Text>
<Text bold isTruncated numberOfLines={2} fontSize="lg">
{songQuery.data!.name}
</Text>
<Text>
<Translate translationKey='level'
format={(level) => `${level}: ${ chaptersQuery.data!.reduce((a, b) => a + b.difficulty, 0) / chaptersQuery.data!.length }`}
<Translate
translationKey="level"
format={(level) =>
`${level}: ${chaptersQuery.data!.reduce((a, b) => a + b.difficulty, 0) /
chaptersQuery.data!.length
}`
}
/>
</Text>
<TextButton translate={{ translationKey: 'playBtn' }} width='auto'
onPress={() => navigation.navigate('Play', { songId: songQuery.data!.id, type: 'normal' })}
rightIcon={<Icon as={Ionicons} name="play-outline"/>}
<TextButton
translate={{ translationKey: "playBtn" }}
width="auto"
onPress={() =>
navigation.navigate("Play", {
songId: songQuery.data!.id,
type: "normal",
})
}
rightIcon={<Icon as={Ionicons} name="play-outline" />}
/>
<TextButton translate={{ translationKey: 'practiceBtn' }} width='auto'
onPress={() => navigation.navigate('Play', { songId: songQuery.data!.id, type: 'practice' })}
rightIcon={<Icon as={Ionicons} name="play-outline"/>}
colorScheme='secondary'
<TextButton
translate={{ translationKey: "practiceBtn" }}
width="auto"
onPress={() =>
navigation.navigate("Play", {
songId: songQuery.data!.id,
type: "practice",
})
}
rightIcon={<Icon as={Ionicons} name="play-outline" />}
colorScheme="secondary"
/>
</Stack>
</Box>
</Box>
<Box style={{ flexDirection: 'row', justifyContent: 'space-between', padding: 30}}>
<Box style={{ flexDirection: 'column', alignItems: 'center' }}>
<Text bold fontSize='lg'>
<Translate translationKey='bestScore'/>
<Box
style={{
flexDirection: "row",
justifyContent: "space-between",
padding: 30,
}}
>
<Box style={{ flexDirection: "column", alignItems: "center" }}>
<Text bold fontSize="lg">
<Translate translationKey="bestScore" />
</Text>
<Text>{scoresQuery.data?.best ?? 0}</Text>
</Box>
<Box style={{ flexDirection: 'column', alignItems: 'center' }}>
<Text bold fontSize='lg'>
<Translate translationKey='lastScore'/>
<Box style={{ flexDirection: "column", alignItems: "center" }}>
<Text bold fontSize="lg">
<Translate translationKey="lastScore" />
</Text>
<Text>{scoresQuery.data?.history.at(0)?.score ?? 0}</Text>
</Box>
</Box>
{/* <Text style={{ paddingBottom: 10 }}>{songQuery.data!.description}</Text> */}
<Box flexDirection='row'>
<Box flexDirection="row">
<TextButton
translate={{ translationKey: 'chapters' }}
variant='ghost'
translate={{ translationKey: "chapters" }}
variant="ghost"
onPress={() => setChaptersOpen(!chaptersOpen)}
endIcon={<Icon as={Ionicons} name={chaptersOpen ? "chevron-up-outline" : "chevron-down-outline"}/>}
endIcon={
<Icon
as={Ionicons}
name={
chaptersOpen ? "chevron-up-outline" : "chevron-down-outline"
}
/>
}
/>
</Box>
<PresenceTransition visible={chaptersOpen} initial={{ opacity: 0 }}>
{ chaptersQuery.isLoading && <LoadingComponent/>}
{ !chaptersQuery.isLoading &&
{chaptersQuery.isLoading && <LoadingComponent />}
{!chaptersQuery.isLoading && (
<VStack flex={1} space={4} padding="4" divider={<Divider />}>
{ chaptersQuery.data!.map((chapter) =>
<Box key={chapter.id} flexGrow={1} flexDirection='row' justifyContent="space-between">
<Text>{chapter.name}</Text>
<Text>
{`${translate('level')} ${chapter.difficulty} - ${formatDuration((chapter.end - chapter.start) * 1000)}`}
</Text>
</Box>
)}
{chaptersQuery.data!.map((chapter) => (
<Box
key={chapter.id}
flexGrow={1}
flexDirection="row"
justifyContent="space-between"
>
<Text>{chapter.name}</Text>
<Text>
{`${translate("level")} ${chapter.difficulty
} - ${formatDuration((chapter.end - chapter.start) * 1000)}`}
</Text>
</Box>
))}
</VStack>
}
)}
</PresenceTransition>
</Box>
)
}
);
};
export default SongLobbyView;