feat: song illustrations code
This commit is contained in:
committed by
Clément Le Bihan
parent
41d020e7a2
commit
6dcda01f6f
@@ -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);
|
||||
|
||||
28
front/API.ts
28
front/API.ts
@@ -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;
|
||||
}
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user