48 Commits

Author SHA1 Message Date
GitBluub
46d427363f one more radioart for later 2024-01-12 12:34:40 +01:00
GitBluub
0b8b58e915 feat: overlay artist image and background for cover 2023-11-16 04:52:38 +01:00
bfb6cf5958 Disable jwt auth for images routes 2023-10-12 13:34:56 +02:00
a92ca75760 Fix dev nginx 2023-10-12 12:47:48 +02:00
8d8323e382 Cleanup inports 2023-10-12 12:47:48 +02:00
76d7e69d19 Add includable fields for all ressources 2023-10-12 12:47:48 +02:00
be58e932a9 Run prettier 2023-10-12 12:47:48 +02:00
38bbe56e9b Add robot tests 2023-10-12 12:47:48 +02:00
a65ce6595a Add a generic include system and implement it for songs 2023-10-12 12:47:48 +02:00
Arthur Jamet
90f7890e5f Update README (#314)
* Update README

* README: Fixes cause me dumb
2023-10-09 16:46:35 +02:00
Arthur Jamet
911e174aef Front: Update splashscreen (#312) 2023-10-08 06:56:16 +02:00
Arthur Jamet
6d7f46c425 Merge pull request #308 from Chroma-Case/front/fix-oops 2023-10-05 12:09:56 +02:00
Arthur Jamet
b72e7a54e5 Front: Fix Oops page 2023-10-05 10:25:51 +02:00
Arthur Jamet
d99d134382 Front: Fix Web Build + Improve CI (#302)
Co-authored-by: Clément Le Bihan <clement.lebihan773@gmail.com>
2023-10-03 14:56:09 +02:00
Arthur Jamet
576675411a Merge pull request #292 from Chroma-Case/front/fix-expo 2023-10-02 18:01:43 +02:00
Arthur Jamet
d214558bc4 Front: Remove unused import 2023-10-02 17:06:02 +02:00
Arthur Jamet
4299a93afe Front: Fix env var 2023-10-02 16:56:46 +02:00
Arthur Jamet
920126a392 Front: Set Env Vars 2023-10-02 14:09:17 +02:00
Arthur Jamet
16e6a5e21b Front: Fix Icon dimensions 2023-10-01 11:40:55 +02:00
Arthur Jamet
9539018b64 .env.example: add new env var 2023-10-01 11:22:06 +02:00
Arthur Jamet
0081eb2acd Front: EAS: Fix project slug 2023-10-01 11:21:20 +02:00
Arthur Jamet
bcb0825f5a Front: remove duplicate deps 2023-09-30 14:28:29 +02:00
Arthur Jamet
18a3fa518c Front: Add missing dependency 2023-09-30 14:07:14 +02:00
Arthur Jamet
0407f5c29e Front: Add missing dependency 2023-09-30 12:07:53 +02:00
Arthur Jamet
6dafe2a8e9 Front: try a custom fork 2023-09-30 11:52:10 +02:00
Arthur Jamet
4a8f0aa1af Front: Add missing eslint deps 2023-09-30 11:17:43 +02:00
Arthur Jamet
745b20358d Front: Add eslint in dev deps 2023-09-30 11:14:04 +02:00
Arthur Jamet
0f544b31f3 Front: Add prettier in dev deps 2023-09-30 11:08:56 +02:00
Arthur Jamet
76d70f3edd Front: Typecheck 2023-09-30 11:05:08 +02:00
Arthur Jamet
1c17ac8b13 Front: Fix missing dependencies 2023-09-30 10:45:23 +02:00
Arthur Jamet
232579e75b Front: Add Dependencies 2023-09-30 10:23:02 +02:00
Arthur Jamet
01221eda00 Front: Add dependencies 2023-09-29 18:34:46 +02:00
Arthur Jamet
b73c2fef58 Front: Add dependencies 2023-09-29 18:17:55 +02:00
Arthur Jamet
3c9c1b5ff7 Front: Add dependencies 2023-09-29 18:00:11 +02:00
Arthur Jamet
e50b1c1344 Front: Install Dev client 2023-09-29 16:03:50 +02:00
Arthur Jamet
6dfc531891 Front: Install Jest 2023-09-29 15:53:20 +02:00
Arthur Jamet
b4f268dee0 Front: Redump Expo 2023-09-29 15:47:16 +02:00
Arthur Jamet
e366fa4b32 Merge pull request #282 from Chroma-Case/feature/adc/retour-utilisateur
Feature/adc/retour utilisateur
2023-09-26 07:31:19 +02:00
Arthur Jamet
cd87451208 Front: Typechecking 2023-09-25 17:55:46 +02:00
danis
845c473ed5 removed useless function 2023-09-25 17:17:05 +02:00
danis
f4d75eef73 css whatever + pretty 2023-09-25 14:51:20 +02:00
danis
5395bbb03a Duration component 2023-09-25 14:24:08 +02:00
danis
2d90c6eec1 pretty 2023-09-22 15:50:26 +02:00
danis
0b0fd0585d added DurationInfo 2023-09-22 15:49:12 +02:00
danis
6cf72dfcca Merge branch 'main' into feature/adc/retour-utilisateur 2023-09-22 15:11:33 +02:00
danis
a81c0b83bb song length SongRow 2023-09-22 15:08:28 +02:00
danis
b2fb497ecf populate.py updated with midi length 2023-09-22 14:53:36 +02:00
445816dfad Fix log error for images 2023-09-21 17:03:18 +02:00
114 changed files with 5078 additions and 19523 deletions

View File

@@ -8,7 +8,8 @@ POSTGRES_DB=chromacase
API_URL=http://localhost:80/api
SCORO_URL=ws://localhost:6543
MINIO_ROOT_PASSWORD=12345678
EXPO_PUBLIC_API_URL=http://localhost:80/api
EXPO_PUBLIC_SCORO_URL=ws://localhost:6543
GOOGLE_CLIENT_ID=toto
GOOGLE_SECRET=tata
GOOGLE_CALLBACK_URL=http://localhost:19006/logged/google

View File

@@ -27,6 +27,25 @@ jobs:
## Build App ##
Check_Front:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./front
environment: Staging
steps:
- uses: actions/checkout@v3
- name: Install Yarn
run: npm install -g yarn
- name: Install dependencies
run: yarn install
- name: Type Check
run: yarn tsc
- name: Check Prettier
run: yarn pretty:check .
- name: Run Linter
run: yarn lint
Build_Front:
runs-on: ubuntu-latest
defaults:
@@ -43,19 +62,21 @@ jobs:
- name: Install dependencies
run: yarn install
- name: Type Check
run: yarn tsc
- name: Check Prettier
run: yarn pretty:check .
- name: Run Linter
run: yarn lint
- name: 🏗 Setup Expo
uses: expo/expo-github-action@v7
with:
expo-version: latest
eas-version: 3.3.1
token: ${{ secrets.EXPO_TOKEN }}
- name: Build Web App
uses: docker/build-push-action@v3
with:
context: ./front
push: false
tags: ${{steps.meta_front.outputs.tags}}
build-args: |
API_URL=${{secrets.API_URL}}
SCORO_URL=${{secrets.SCORO_URL}}
- name: Build Android APK
run: |

View File

@@ -1,9 +1,39 @@
# ![Chromacase](./assets/graphical/title.png)
# ![Chromacase](./assets/graphical/banner.png)
La principale raison pour laquelle on arrête de jouer d'un instrument est la perte de motivation. C'est un apprentissage long et vraiment demandant. ChromaCase propose d'accompagner les joueurs de piano grâce à une application mobile avec une expérience personnalisée. Celle-ci, générée par une IA, cible les goûts et identifie les difficultés du joueur.
Ça vous interesse? Rendez-vous sur notre [site](https://chromacase.studio/) pour prendre contact
Ça vous interesse? Rendez-vous sur notre [site](http://eip.epitech.eu/2024/chromacase) pour prendre contact
## Structure du Projet
## Comment lancer le projet
![Schéma Fonctionnel](./assets/docs/structure.png)
Pensez à remplir un `.env` (à la racine du projet), en se basant sur le `.env.example`.
### Development
```bash
docker-compose -f docker-compose.dev.yml up --build
```
### Production
```bash
docker-compose up --build
```
## Liens Utiles
- Site de Production: [Lien](http://chroma.octohub.app/)
- Site du Nightly: [Lien](http://nightly.chroma.octohub.app/)
- Site vitrine: [Lien](http://eip.epitech.eu/2024/chromacase)
- Documentation: [Github](https://github.com/Chroma-Case/DAteX)
## Membres du Projet
| Nom | Role | Contact |
|--------------------------|--------------------------------------|----------------------------------------------------|
| Zoé Roux | CEO, Responsable Back-end | [GitHub](https://github.com/zoriya) |
| Clément Le-Bihan | CTO, Responsable Front-end | [GitHub](https://github.com/Octopus773) |
| Arthur Jamet | Manager, Développeur Front-end | [GitHub](https://github.com/Arthi-chaud) |
| Louis Auzuret | Développeur Back-end, Responsable CI | [Github](https://github.com/GitBluub) |
| Aumaury Danis-Cousandier | Développeur Front-end | [Github](https://github.com/AmauryDanisCousandier) |
| Mathys Paul | Développeur Front-end, Designer | [GitHub](https://github.com/mathysPaul) |

BIN
assets/graphical/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 KiB

View File

@@ -4,6 +4,7 @@ import sys
import os
import requests
import glob
from mido import MidiFile
from configparser import ConfigParser
url = os.environ.get("API_URL")
@@ -20,16 +21,16 @@ def getOrCreateAlbum(name, artistId):
return out["id"]
def getOrCreateGenre(names):
ids = []
for name in names.split(","):
res = requests.post(f"{url}/genre", json={
"name": name,
})
out = res.json()
print(out)
ids += [out["id"]]
#TODO handle multiple genres
return ids[0]
ids = []
for name in names.split(","):
res = requests.post(f"{url}/genre", json={
"name": name,
})
out = res.json()
print(out)
ids += [out["id"]]
#TODO handle multiple genres
return ids[0]
def getOrCreateArtist(name):
res = requests.post(f"{url}/artist", json={
@@ -42,8 +43,10 @@ def getOrCreateArtist(name):
def populateFile(path, midi, mxl):
config = ConfigParser()
config.read(path)
mid = MidiFile(midi)
metadata = config["Metadata"];
difficulties = dict(config["Difficulties"])
difficulties["length"] = round((mid.length), 2)
artistId = getOrCreateArtist(metadata["Artist"])
print(f"Populating {metadata['Name']}")
res = requests.post(f"{url}/song", json={
@@ -58,7 +61,6 @@ def populateFile(path, midi, mxl):
})
print(res.json())
def main():
global url
if url == None:

BIN
back/artist.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
back/background_cover.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
back/icon_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

10950
back/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@jimp/plugin-circle": "^0.22.10",
"@nestjs-modules/mailer": "^1.9.1",
"@nestjs/common": "^10.1.0",
"@nestjs/config": "^3.0.0",
@@ -36,9 +37,9 @@
"@types/passport": "^1.0.12",
"bcryptjs": "^2.4.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.13.2",
"json-logger-service": "^9.0.1",
"class-validator": "^0.14.0",
"jimp": "^0.22.10",
"json-logger-service": "^9.0.1",
"node-fetch": "^2.6.12",
"nodemailer": "^6.9.5",
"passport-google-oauth20": "^2.0.0",

BIN
back/radioart.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
back/radioart2.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
back/radioart3.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

BIN
back/radioart4.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,5 +1,4 @@
import {
BadRequestException,
Body,
ConflictException,
Controller,
@@ -12,6 +11,7 @@ import {
Post,
Query,
Req,
UseGuards,
} from '@nestjs/common';
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
import { CreateAlbumDto } from './dto/create-album.dto';
@@ -21,16 +21,25 @@ import { Prisma, Album } from '@prisma/client';
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { FilterQuery } from 'src/utils/filter.pipe';
import { Album as _Album } from 'src/_gen/prisma-class/album';
import { IncludeMap, mapInclude } from 'src/utils/include';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
@Controller('album')
@ApiTags('album')
@UseGuards(JwtAuthGuard)
export class AlbumController {
static filterableFields: string[] = ['+id', 'name', '+artistId'];
static includableFields: IncludeMap<Prisma.AlbumInclude> = {
artist: true,
Song: true,
};
constructor(private readonly albumService: AlbumService) {}
@Post()
@ApiOperation({ description: "Register a new album, should not be used by frontend"})
@ApiOperation({
description: 'Register a new album, should not be used by frontend',
})
async create(@Body() createAlbumDto: CreateAlbumDto) {
try {
return await this.albumService.createAlbum({
@@ -47,7 +56,7 @@ export class AlbumController {
}
@Delete(':id')
@ApiOperation({ description: "Delete an album by id"})
@ApiOperation({ description: 'Delete an album by id' })
async remove(@Param('id', ParseIntPipe) id: number) {
try {
return await this.albumService.deleteAlbum({ id });
@@ -58,11 +67,12 @@ export class AlbumController {
@Get()
@ApiOkResponsePlaginated(_Album)
@ApiOperation({ description: "Get all albums paginated"})
@ApiOperation({ description: 'Get all albums paginated' })
async findAll(
@Req() req: Request,
@FilterQuery(AlbumController.filterableFields)
where: Prisma.AlbumWhereInput,
@Query('include') include: string,
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Album>> {
@@ -70,15 +80,23 @@ export class AlbumController {
skip,
take,
where,
include: mapInclude(include, req, AlbumController.includableFields),
});
return new Plage(ret, req);
}
@Get(':id')
@ApiOperation({ description: "Get an album by id"})
@ApiOkResponse({ type: _Album})
async findOne(@Param('id', ParseIntPipe) id: number) {
const res = await this.albumService.album({ id });
@ApiOperation({ description: 'Get an album by id' })
@ApiOkResponse({ type: _Album })
async findOne(
@Req() req: Request,
@Query('include') include: string,
@Param('id', ParseIntPipe) id: number,
) {
const res = await this.albumService.album(
{ id },
mapInclude(include, req, AlbumController.includableFields),
);
if (res === null) throw new NotFoundException('Album not found');
return res;

View File

@@ -14,9 +14,11 @@ export class AlbumService {
async album(
albumWhereUniqueInput: Prisma.AlbumWhereUniqueInput,
include?: Prisma.AlbumInclude,
): Promise<Album | null> {
return this.prisma.album.findUnique({
where: albumWhereUniqueInput,
include,
});
}
@@ -26,14 +28,16 @@ export class AlbumService {
cursor?: Prisma.AlbumWhereUniqueInput;
where?: Prisma.AlbumWhereInput;
orderBy?: Prisma.AlbumOrderByWithRelationInput;
include?: Prisma.AlbumInclude;
}): Promise<Album[]> {
const { skip, take, cursor, where, orderBy } = params;
const { skip, take, cursor, where, orderBy, include } = params;
return this.prisma.album.findMany({
skip,
take,
cursor,
where,
orderBy,
include,
});
}

View File

@@ -7,7 +7,9 @@ export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
@ApiOkResponse({ description: 'Return a hello world message, used as a health route' })
@ApiOkResponse({
description: 'Return a hello world message, used as a health route',
})
getHello(): string {
return this.appService.getHello();
}

View File

@@ -1,5 +1,4 @@
import {
BadRequestException,
Body,
ConflictException,
Controller,
@@ -14,26 +13,42 @@ import {
Query,
Req,
StreamableFile,
UseGuards,
} from '@nestjs/common';
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
import { CreateArtistDto } from './dto/create-artist.dto';
import { Request } from 'express';
import { ArtistService } from './artist.service';
import { Prisma, Artist } from '@prisma/client';
import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import {
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { createReadStream, existsSync } from 'fs';
import { FilterQuery } from 'src/utils/filter.pipe';
import { Artist as _Artist} from 'src/_gen/prisma-class/artist';
import { Artist as _Artist } from 'src/_gen/prisma-class/artist';
import { IncludeMap, mapInclude } from 'src/utils/include';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { Public } from 'src/auth/public';
@Controller('artist')
@ApiTags('artist')
@UseGuards(JwtAuthGuard)
export class ArtistController {
static filterableFields = ['+id', 'name'];
static includableFields: IncludeMap<Prisma.ArtistInclude> = {
Song: true,
Album: true,
};
constructor(private readonly service: ArtistService) {}
@Post()
@ApiOperation({ description: "Register a new artist, should not be used by frontend"})
@ApiOperation({
description: 'Register a new artist, should not be used by frontend',
})
async create(@Body() dto: CreateArtistDto) {
try {
return await this.service.create(dto);
@@ -43,7 +58,7 @@ export class ArtistController {
}
@Delete(':id')
@ApiOperation({ description: "Delete an artist by id"})
@ApiOperation({ description: 'Delete an artist by id' })
async remove(@Param('id', ParseIntPipe) id: number) {
try {
return await this.service.delete({ id });
@@ -53,8 +68,9 @@ export class ArtistController {
}
@Get(':id/illustration')
@ApiOperation({ description: "Get an artist's illustration"})
@ApiNotFoundResponse({ description: "Artist or illustration not found"})
@ApiOperation({ description: "Get an artist's illustration" })
@ApiNotFoundResponse({ description: 'Artist or illustration not found' })
@Public()
async getIllustration(@Param('id', ParseIntPipe) id: number) {
const artist = await this.service.get({ id });
if (!artist) throw new NotFoundException('Artist not found');
@@ -71,12 +87,13 @@ export class ArtistController {
}
@Get()
@ApiOperation({ description: "Get all artists paginated"})
@ApiOperation({ description: 'Get all artists paginated' })
@ApiOkResponsePlaginated(_Artist)
async findAll(
@Req() req: Request,
@FilterQuery(ArtistController.filterableFields)
where: Prisma.ArtistWhereInput,
@Query('include') include: string,
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Artist>> {
@@ -84,15 +101,23 @@ export class ArtistController {
skip,
take,
where,
include: mapInclude(include, req, ArtistController.includableFields),
});
return new Plage(ret, req);
}
@Get(':id')
@ApiOperation({ description: "Get an artist by id"})
@ApiOkResponse({ type: _Artist})
async findOne(@Param('id', ParseIntPipe) id: number) {
const res = await this.service.get({ id });
@ApiOperation({ description: 'Get an artist by id' })
@ApiOkResponse({ type: _Artist })
async findOne(
@Req() req: Request,
@Query('include') include: string,
@Param('id', ParseIntPipe) id: number,
) {
const res = await this.service.get(
{ id },
mapInclude(include, req, ArtistController.includableFields),
);
if (res === null) throw new NotFoundException('Artist not found');
return res;

View File

@@ -12,9 +12,13 @@ export class ArtistService {
});
}
async get(where: Prisma.ArtistWhereUniqueInput): Promise<Artist | null> {
async get(
where: Prisma.ArtistWhereUniqueInput,
include?: Prisma.ArtistInclude,
): Promise<Artist | null> {
return this.prisma.artist.findUnique({
where,
include,
});
}
@@ -24,14 +28,16 @@ export class ArtistService {
cursor?: Prisma.ArtistWhereUniqueInput;
where?: Prisma.ArtistWhereInput;
orderBy?: Prisma.ArtistOrderByWithRelationInput;
include?: Prisma.ArtistInclude;
}): Promise<Artist[]> {
const { skip, take, cursor, where, orderBy } = params;
const { skip, take, cursor, where, orderBy, include } = params;
return this.prisma.artist.findMany({
skip,
take,
cursor,
where,
orderBy,
include,
});
}

View File

@@ -32,11 +32,8 @@ import {
ApiBearerAuth,
ApiBody,
ApiConflictResponse,
ApiCreatedResponse,
ApiNoContentResponse,
ApiOkResponse,
ApiOperation,
ApiResponse,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
@@ -63,11 +60,14 @@ export class AuthController {
@Get('login/google')
@UseGuards(AuthGuard('google'))
@ApiOperation({description: 'Redirect to google login page'})
@ApiOperation({ description: 'Redirect to google login page' })
googleLogin() {}
@Get('logged/google')
@ApiOperation({description: 'Redirect to the front page after connecting to the google account'})
@ApiOperation({
description:
'Redirect to the front page after connecting to the google account',
})
@UseGuards(AuthGuard('google'))
async googleLoginCallbakc(@Req() req: any) {
let user = await this.usersService.user({ googleID: req.user.googleID });
@@ -79,9 +79,11 @@ export class AuthController {
}
@Post('register')
@ApiOperation({description: 'Register a new user'})
@ApiOperation({ description: 'Register a new user' })
@ApiConflictResponse({ description: 'Username or email already taken' })
@ApiOkResponse({ description: 'Successfully registered, email sent to verify' })
@ApiOkResponse({
description: 'Successfully registered, email sent to verify',
})
@ApiBadRequestResponse({ description: 'Invalid data or database error' })
async register(@Body() registerDto: RegisterDto): Promise<void> {
try {
@@ -101,19 +103,21 @@ export class AuthController {
@Put('verify')
@HttpCode(200)
@UseGuards(JwtAuthGuard)
@ApiOperation({description: 'Verify the email of the user'})
@ApiOperation({ description: 'Verify the email of the user' })
@ApiOkResponse({ description: 'Successfully verified' })
@ApiBadRequestResponse({ description: 'Invalid or expired token' })
async verify(@Request() req: any, @Query('token') token: string): Promise<void> {
if (await this.authService.verifyMail(req.user.id, token))
return;
throw new BadRequestException("Invalid token. Expired or invalid.");
async verify(
@Request() req: any,
@Query('token') token: string,
): Promise<void> {
if (await this.authService.verifyMail(req.user.id, token)) return;
throw new BadRequestException('Invalid token. Expired or invalid.');
}
@Put('reverify')
@UseGuards(JwtAuthGuard)
@HttpCode(200)
@ApiOperation({description: 'Resend the verification email'})
@ApiOperation({ description: 'Resend the verification email' })
async reverify(@Request() req: any): Promise<void> {
const user = await this.usersService.user({ id: req.user.id });
if (!user) throw new BadRequestException('Invalid user');
@@ -277,42 +281,28 @@ export class AuthController {
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ description: 'Successfully added liked song'})
@ApiOkResponse({ description: 'Successfully added liked song' })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Post('me/likes/:id')
addLikedSong(
@Request() req: any,
@Param('id') songId: number
) {
return this.usersService.addLikedSong(
+req.user.id,
+songId,
);
addLikedSong(@Request() req: any, @Param('id') songId: number) {
return this.usersService.addLikedSong(+req.user.id, +songId);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ description: 'Successfully removed liked song'})
@ApiOkResponse({ description: 'Successfully removed liked song' })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Delete('me/likes/:id')
removeLikedSong(
@Request() req: any,
@Param('id') songId: number,
) {
return this.usersService.removeLikedSong(
+req.user.id,
+songId,
);
removeLikedSong(@Request() req: any, @Param('id') songId: number) {
return this.usersService.removeLikedSong(+req.user.id, +songId);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ description: 'Successfully retrieved liked song'})
@ApiOkResponse({ description: 'Successfully retrieved liked song' })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Get('me/likes')
getLikedSongs(
@Request() req: any,
) {
return this.usersService.getLikedSongs(+req.user.id)
getLikedSongs(@Request() req: any) {
return this.usersService.getLikedSongs(+req.user.id);
}
}

View File

@@ -79,7 +79,7 @@ export class AuthService {
console.log('Password reset token failure', e);
return false;
}
console.log(verified)
console.log(verified);
await this.userService.updateUser({
where: { id: verified.userId },
data: { password: new_password },

View File

@@ -1,5 +1,24 @@
import { Injectable } from '@nestjs/common';
import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from './public';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
console.log(context);
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
console.log(isPublic);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}

4
back/src/auth/public.ts Normal file
View File

@@ -0,0 +1,4 @@
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

View File

@@ -13,6 +13,7 @@ import {
Query,
Req,
StreamableFile,
UseGuards,
} from '@nestjs/common';
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
import { CreateGenreDto } from './dto/create-genre.dto';
@@ -23,11 +24,18 @@ import { ApiTags } from '@nestjs/swagger';
import { createReadStream, existsSync } from 'fs';
import { FilterQuery } from 'src/utils/filter.pipe';
import { Genre as _Genre } from 'src/_gen/prisma-class/genre';
import { IncludeMap, mapInclude } from 'src/utils/include';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { Public } from 'src/auth/public';
@Controller('genre')
@ApiTags('genre')
@UseGuards(JwtAuthGuard)
export class GenreController {
static filterableFields: string[] = ['+id', 'name'];
static includableFields: IncludeMap<Prisma.GenreInclude> = {
Song: true,
};
constructor(private readonly service: GenreService) {}
@@ -50,6 +58,7 @@ export class GenreController {
}
@Get(':id/illustration')
@Public()
async getIllustration(@Param('id', ParseIntPipe) id: number) {
const genre = await this.service.get({ id });
if (!genre) throw new NotFoundException('Genre not found');
@@ -71,6 +80,7 @@ export class GenreController {
@Req() req: Request,
@FilterQuery(GenreController.filterableFields)
where: Prisma.GenreWhereInput,
@Query('include') include: string,
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Genre>> {
@@ -78,13 +88,21 @@ export class GenreController {
skip,
take,
where,
include: mapInclude(include, req, GenreController.includableFields),
});
return new Plage(ret, req);
}
@Get(':id')
async findOne(@Param('id', ParseIntPipe) id: number) {
const res = await this.service.get({ id });
async findOne(
@Req() req: Request,
@Query('include') include: string,
@Param('id', ParseIntPipe) id: number,
) {
const res = await this.service.get(
{ id },
mapInclude(include, req, GenreController.includableFields),
);
if (res === null) throw new NotFoundException('Genre not found');
return res;

View File

@@ -12,9 +12,13 @@ export class GenreService {
});
}
async get(where: Prisma.GenreWhereUniqueInput): Promise<Genre | null> {
async get(
where: Prisma.GenreWhereUniqueInput,
include?: Prisma.GenreInclude,
): Promise<Genre | null> {
return this.prisma.genre.findUnique({
where,
include,
});
}
@@ -24,14 +28,16 @@ export class GenreService {
cursor?: Prisma.GenreWhereUniqueInput;
where?: Prisma.GenreWhereInput;
orderBy?: Prisma.GenreOrderByWithRelationInput;
include?: Prisma.GenreInclude;
}): Promise<Genre[]> {
const { skip, take, cursor, where, orderBy } = params;
const { skip, take, cursor, where, orderBy, include } = params;
return this.prisma.genre.findMany({
skip,
take,
cursor,
where,
orderBy,
include,
});
}

View File

@@ -10,14 +10,20 @@ import {
Request,
UseGuards,
} from '@nestjs/common';
import { ApiCreatedResponse, ApiOkResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger';
import {
ApiCreatedResponse,
ApiOkResponse,
ApiOperation,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { SearchHistory, SongHistory } from '@prisma/client';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { SongHistoryDto } from './dto/SongHistoryDto';
import { HistoryService } from './history.service';
import { SearchHistoryDto } from './dto/SearchHistoryDto';
import { SongHistory as _SongHistory } from 'src/_gen/prisma-class/song_history';
import { SearchHistory as _SearchHistory} from 'src/_gen/prisma-class/search_history';
import { SearchHistory as _SearchHistory } from 'src/_gen/prisma-class/search_history';
@Controller('history')
@ApiTags('history')
@@ -26,9 +32,9 @@ export class HistoryController {
@Get()
@HttpCode(200)
@ApiOperation({ description: "Get song history of connected user"})
@ApiOperation({ description: 'Get song history of connected user' })
@UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: _SongHistory, isArray: true})
@ApiOkResponse({ type: _SongHistory, isArray: true })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
async getHistory(
@Request() req: any,
@@ -40,9 +46,9 @@ export class HistoryController {
@Get('search')
@HttpCode(200)
@ApiOperation({ description: "Get search history of connected user"})
@ApiOperation({ description: 'Get search history of connected user' })
@UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: _SearchHistory, isArray: true})
@ApiOkResponse({ type: _SearchHistory, isArray: true })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
async getSearchHistory(
@Request() req: any,
@@ -54,15 +60,15 @@ export class HistoryController {
@Post()
@HttpCode(201)
@ApiOperation({ description: "Create a record of a song played by a user"})
@ApiCreatedResponse({ description: "Succesfully created a record"})
@ApiOperation({ description: 'Create a record of a song played by a user' })
@ApiCreatedResponse({ description: 'Succesfully created a record' })
async create(@Body() record: SongHistoryDto): Promise<SongHistory> {
return this.historyService.createSongHistoryRecord(record);
}
@Post('search')
@HttpCode(201)
@ApiOperation({ description: "Creates a search record in the users history"})
@ApiOperation({ description: 'Creates a search record in the users history' })
@UseGuards(JwtAuthGuard)
@ApiUnauthorizedResponse({ description: 'Invalid token' })
async createSearchHistory(

View File

@@ -3,7 +3,6 @@ import {
Get,
Query,
Req,
Request,
Param,
ParseIntPipe,
DefaultValuePipe,
@@ -12,13 +11,17 @@ import {
Body,
Delete,
NotFoundException,
UseGuards,
} from '@nestjs/common';
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
import { LessonService } from './lesson.service';
import { ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
import { Prisma, Skill } from '@prisma/client';
import { FilterQuery } from 'src/utils/filter.pipe';
import { Lesson as _Lesson} from 'src/_gen/prisma-class/lesson';
import { Lesson as _Lesson } from 'src/_gen/prisma-class/lesson';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { IncludeMap, mapInclude } from 'src/utils/include';
import { Request } from 'express';
export class Lesson {
@ApiProperty()
@@ -35,6 +38,7 @@ export class Lesson {
@ApiTags('lessons')
@Controller('lesson')
@UseGuards(JwtAuthGuard)
export class LessonController {
static filterableFields: string[] = [
'+id',
@@ -42,6 +46,9 @@ export class LessonController {
'+requiredLevel',
'mainSkill',
];
static includableFields: IncludeMap<Prisma.LessonInclude> = {
LessonHistory: true,
};
constructor(private lessonService: LessonService) {}
@@ -54,6 +61,7 @@ export class LessonController {
@Req() request: Request,
@FilterQuery(LessonController.filterableFields)
where: Prisma.LessonWhereInput,
@Query('include') include: string,
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Lesson>> {
@@ -61,6 +69,7 @@ export class LessonController {
skip,
take,
where,
include: mapInclude(include, request, LessonController.includableFields),
});
return new Plage(ret, request);
}
@@ -69,8 +78,15 @@ export class LessonController {
summary: 'Get a particular lessons',
})
@Get(':id')
async get(@Param('id', ParseIntPipe) id: number): Promise<Lesson> {
const ret = await this.lessonService.get(id);
async get(
@Req() req: Request,
@Query('include') include: string,
@Param('id', ParseIntPipe) id: number,
): Promise<Lesson> {
const ret = await this.lessonService.get(
id,
mapInclude(include, req, LessonController.includableFields),
);
if (!ret) throw new NotFoundException();
return ret;
}

View File

@@ -12,22 +12,28 @@ export class LessonService {
cursor?: Prisma.LessonWhereUniqueInput;
where?: Prisma.LessonWhereInput;
orderBy?: Prisma.LessonOrderByWithRelationInput;
include?: Prisma.LessonInclude;
}): Promise<Lesson[]> {
const { skip, take, cursor, where, orderBy } = params;
const { skip, take, cursor, where, orderBy, include } = params;
return this.prisma.lesson.findMany({
skip,
take,
cursor,
where,
orderBy,
include,
});
}
async get(id: number): Promise<Lesson | null> {
async get(
id: number,
include?: Prisma.LessonInclude,
): Promise<Lesson | null> {
return this.prisma.lesson.findFirst({
where: {
id: id,
},
include,
});
}

View File

@@ -10,7 +10,7 @@ import {
} from '@nestjs/common';
import { RequestLogger, RequestLoggerOptions } from 'json-logger-service';
import { tap } from 'rxjs';
import { PrismaModel } from './_gen/prisma-class'
import { PrismaModel } from './_gen/prisma-class';
import { PrismaService } from './prisma/prisma.service';
@Injectable()
@@ -32,15 +32,14 @@ export class AspectLogger implements NestInterceptor {
};
return next.handle().pipe(
tap((data) =>
tap((/* data */) =>
console.log(
JSON.stringify({
...toPrint,
statusCode,
data,
//data, //TODO: Data crashed with images
}),
),
),
),),
);
}
}
@@ -59,7 +58,9 @@ async function bootstrap() {
.setDescription('The chromacase API')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config, { extraModels: [...PrismaModel.extraModels]});
const document = SwaggerModule.createDocument(app, config, {
extraModels: [...PrismaModel.extraModels],
});
SwaggerModule.setup('api', app, document);
app.useGlobalPipes(new ValidationPipe());

View File

@@ -3,14 +3,28 @@
*/
import { Type, applyDecorators } from '@nestjs/common';
import { ApiExtraModels, ApiOkResponse, ApiProperty, getSchemaPath } from '@nestjs/swagger';
import {
ApiExtraModels,
ApiOkResponse,
ApiProperty,
getSchemaPath,
} from '@nestjs/swagger';
export class PlageMetadata {
@ApiProperty()
this: string;
@ApiProperty({ type: "string", nullable: true, description: "null if there is no next page, couldn't set it in swagger"})
@ApiProperty({
type: 'string',
nullable: true,
description: "null if there is no next page, couldn't set it in swagger",
})
next: string | null;
@ApiProperty({ type: "string", nullable: true, description: "null if there is no previous page, couldn't set it in swagger" })
@ApiProperty({
type: 'string',
nullable: true,
description:
"null if there is no previous page, couldn't set it in swagger",
})
previous: string | null;
}
@@ -55,22 +69,24 @@ export class Plage<T extends object> {
}
}
export const ApiOkResponsePlaginated = <DataDto extends Type<unknown>>(dataDto: DataDto) =>
applyDecorators(
ApiExtraModels(Plage, dataDto),
ApiOkResponse({
schema: {
allOf: [
{ $ref: getSchemaPath(Plage) },
{
properties: {
data: {
type: 'array',
items: { $ref: getSchemaPath(dataDto) },
},
},
},
],
},
})
)
export const ApiOkResponsePlaginated = <DataDto extends Type<unknown>>(
dataDto: DataDto,
) =>
applyDecorators(
ApiExtraModels(Plage, dataDto),
ApiOkResponse({
schema: {
allOf: [
{ $ref: getSchemaPath(Plage) },
{
properties: {
data: {
type: 'array',
items: { $ref: getSchemaPath(dataDto) },
},
},
},
],
},
}),
);

View File

@@ -1,25 +1,29 @@
import {
BadRequestException,
Body,
Controller,
Get,
HttpCode,
InternalServerErrorException,
NotFoundException,
Param,
ParseIntPipe,
Post,
Query,
Request,
UseGuards,
} from '@nestjs/common';
import { ApiOkResponse, ApiOperation, ApiParam, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger';
import {
ApiOkResponse,
ApiOperation,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { Artist, Genre, Song } from '@prisma/client';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { SearchSongDto } from './dto/search-song.dto';
import { SearchService } from './search.service';
import { Song as _Song } from 'src/_gen/prisma-class/song';
import { Genre as _Genre } from 'src/_gen/prisma-class/genre';
import { Artist as _Artist } from 'src/_gen/prisma-class/artist';
import { mapInclude } from 'src/utils/include';
import { SongController } from 'src/song/song.controller';
import { GenreController } from 'src/genre/genre.controller';
import { ArtistController } from 'src/artist/artist.controller';
@ApiTags('search')
@Controller('search')
@@ -27,16 +31,21 @@ export class SearchController {
constructor(private readonly searchService: SearchService) {}
@Get('songs/:query')
@ApiOkResponse({ type: _Song, isArray: true})
@ApiOperation({ description: "Search a song"})
@ApiUnauthorizedResponse({ description: "Invalid token"})
@ApiOkResponse({ type: _Song, isArray: true })
@ApiOperation({ description: 'Search a song' })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@UseGuards(JwtAuthGuard)
async searchSong(
@Request() req: any,
@Query('include') include: string,
@Param('query') query: string,
): Promise<Song[] | null> {
try {
const ret = await this.searchService.songByGuess(query, req.user?.id);
const ret = await this.searchService.songByGuess(
query,
req.user?.id,
mapInclude(include, req, SongController.includableFields),
);
if (!ret.length) throw new NotFoundException();
else return ret;
} catch (error) {
@@ -46,12 +55,20 @@ export class SearchController {
@Get('genres/:query')
@UseGuards(JwtAuthGuard)
@ApiUnauthorizedResponse({ description: "Invalid token"})
@ApiOkResponse({ type: _Genre, isArray: true})
@ApiOperation({ description: "Search a genre"})
async searchGenre(@Request() req: any, @Param('query') query: string): Promise<Genre[] | null> {
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@ApiOkResponse({ type: _Genre, isArray: true })
@ApiOperation({ description: 'Search a genre' })
async searchGenre(
@Request() req: any,
@Query('include') include: string,
@Param('query') query: string,
): Promise<Genre[] | null> {
try {
const ret = await this.searchService.genreByGuess(query, req.user?.id);
const ret = await this.searchService.genreByGuess(
query,
req.user?.id,
mapInclude(include, req, GenreController.includableFields),
);
if (!ret.length) throw new NotFoundException();
else return ret;
} catch (error) {
@@ -61,12 +78,20 @@ export class SearchController {
@Get('artists/:query')
@UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: _Artist, isArray: true})
@ApiUnauthorizedResponse({ description: "Invalid token"})
@ApiOperation({ description: "Search an artist"})
async searchArtists(@Request() req: any, @Param('query') query: string): Promise<Artist[] | null> {
@ApiOkResponse({ type: _Artist, isArray: true })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@ApiOperation({ description: 'Search an artist' })
async searchArtists(
@Request() req: any,
@Query('include') include: string,
@Param('query') query: string,
): Promise<Artist[] | null> {
try {
const ret = await this.searchService.artistByGuess(query, req.user?.id);
const ret = await this.searchService.artistByGuess(
query,
req.user?.id,
mapInclude(include, req, ArtistController.includableFields),
);
if (!ret.length) throw new NotFoundException();
else return ret;
} catch (error) {

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@nestjs/common';
import { Album, Artist, Prisma, Song, Genre } from '@prisma/client';
import { Artist, Prisma, Song, Genre } from '@prisma/client';
import { HistoryService } from 'src/history/history.service';
import { PrismaService } from 'src/prisma/prisma.service';
@@ -10,27 +10,42 @@ export class SearchService {
private history: HistoryService,
) {}
async songByGuess(query: string, userID: number): Promise<Song[]> {
async songByGuess(
query: string,
userID: number,
include?: Prisma.SongInclude,
): Promise<Song[]> {
return this.prisma.song.findMany({
where: {
name: { contains: query, mode: 'insensitive' },
},
include,
});
}
async genreByGuess(query: string, userID: number): Promise<Genre[]> {
async genreByGuess(
query: string,
userID: number,
include?: Prisma.GenreInclude,
): Promise<Genre[]> {
return this.prisma.genre.findMany({
where: {
name: { contains: query, mode: 'insensitive' },
},
include,
});
}
async artistByGuess(query: string, userID: number): Promise<Artist[]> {
async artistByGuess(
query: string,
userID: number,
include?: Prisma.ArtistInclude,
): Promise<Artist[]> {
return this.prisma.artist.findMany({
where: {
name: { contains: query, mode: 'insensitive' },
},
include,
});
}
}

View File

@@ -22,23 +22,36 @@ import { SongService } from './song.service';
import { Request } from 'express';
import { Prisma, Song } from '@prisma/client';
import { createReadStream, existsSync } from 'fs';
import { ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiProperty, ApiResponse, ApiResponseProperty, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger';
import {
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiProperty,
ApiTags,
ApiUnauthorizedResponse,
} from '@nestjs/swagger';
import { HistoryService } from 'src/history/history.service';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { FilterQuery } from 'src/utils/filter.pipe';
import { Song as _Song } from 'src/_gen/prisma-class/song';
import { SongHistory } from 'src/_gen/prisma-class/song_history';
import { IncludeMap, mapInclude } from 'src/utils/include';
import { Public } from 'src/auth/public';
import Jimp from 'jimp';
class SongHistoryResult {
@ApiProperty()
best: number;
@ApiProperty({ type: SongHistory, isArray: true})
@ApiProperty({ type: SongHistory, isArray: true })
history: SongHistory[];
}
const BACKGROUND_COVER = 'radioart3.jpeg';
const ICON = 'icon_dark.png';
@Controller('song')
@ApiTags('song')
@UseGuards(JwtAuthGuard)
export class SongController {
static filterableFields: string[] = [
'+id',
@@ -47,6 +60,13 @@ export class SongController {
'+albumId',
'+genreId',
];
static includableFields: IncludeMap<Prisma.SongInclude> = {
artist: true,
album: true,
genre: true,
SongHistory: ({ user }) => ({ where: { userID: user.id } }),
likedByUsers: ({ user }) => ({ where: { userId: user.id } }),
};
constructor(
private readonly songService: SongService,
@@ -54,9 +74,9 @@ export class SongController {
) {}
@Get(':id/midi')
@ApiOperation({ description: "Streams the midi file of the requested song"})
@ApiNotFoundResponse({ description: "Song not found"})
@ApiOkResponse({ description: "Returns the midi file succesfully"})
@ApiOperation({ description: 'Streams the midi file of the requested song' })
@ApiNotFoundResponse({ description: 'Song not found' })
@ApiOkResponse({ description: 'Returns the midi file succesfully' })
async getMidi(@Param('id', ParseIntPipe) id: number) {
const song = await this.songService.song({ id });
if (!song) throw new NotFoundException('Song not found');
@@ -69,18 +89,39 @@ export class SongController {
}
}
async gen_illustration(song: Song) {
const img = await Jimp.read(BACKGROUND_COVER);
// @ts-ignore
const artist_img = await Jimp.read(`/assets/artists/${song.artist.name}/illustration.png`);
const logo = await Jimp.read(ICON);
img.cover(600, 600);
artist_img.cover(400, 400);
logo.cover(70, 70);
artist_img.circle();
img.composite(artist_img, 100, 100);
img.composite(logo, 10, 10);
return img;
}
@Get(':id/illustration')
@ApiOperation({ description: "Streams the illustration of the requested song"})
@ApiNotFoundResponse({ description: "Song not found"})
@ApiOkResponse({ description: "Returns the illustration succesfully"})
@ApiOperation({
description: 'Streams the illustration of the requested song',
})
@ApiNotFoundResponse({ description: 'Song not found' })
@ApiOkResponse({ description: 'Returns the illustration succesfully' })
@Public()
async getIllustration(@Param('id', ParseIntPipe) id: number) {
const song = await this.songService.song({ id });
const song = await this.songService.song({ id }, { artist: true } );
if (!song) throw new NotFoundException('Song not found');
//await this.gen_illustration(song);
if (song.illustrationPath === null) throw new NotFoundException();
if (!existsSync(song.illustrationPath))
throw new NotFoundException('Illustration not found');
if (!existsSync(song.illustrationPath)) {
let img = await this.gen_illustration(song);
img.write(song.illustrationPath);
}
try {
const file = createReadStream(song.illustrationPath);
return new StreamableFile(file);
@@ -90,9 +131,11 @@ export class SongController {
}
@Get(':id/musicXml')
@ApiOperation({ description: "Streams the musicXML file of the requested song"})
@ApiNotFoundResponse({ description: "Song not found"})
@ApiOkResponse({ description: "Returns the musicXML file succesfully"})
@ApiOperation({
description: 'Streams the musicXML file of the requested song',
})
@ApiNotFoundResponse({ description: 'Song not found' })
@ApiOkResponse({ description: 'Returns the musicXML file succesfully' })
async getMusicXml(@Param('id', ParseIntPipe) id: number) {
const song = await this.songService.song({ id });
if (!song) throw new NotFoundException('Song not found');
@@ -102,7 +145,10 @@ export class SongController {
}
@Post()
@ApiOperation({description: "register a new song in the database, should not be used by the frontend"})
@ApiOperation({
description:
'register a new song in the database, should not be used by the frontend',
})
async create(@Body() createSongDto: CreateSongDto) {
try {
return await this.songService.createSong({
@@ -118,7 +164,6 @@ export class SongController {
: undefined,
});
} catch {
throw new ConflictException(
await this.songService.song({ name: createSongDto.name }),
);
@@ -126,7 +171,7 @@ export class SongController {
}
@Delete(':id')
@ApiOperation({ description: "delete a song by id"})
@ApiOperation({ description: 'delete a song by id' })
async remove(@Param('id', ParseIntPipe) id: number) {
try {
return await this.songService.deleteSong({ id });
@@ -140,6 +185,7 @@ export class SongController {
async findAll(
@Req() req: Request,
@FilterQuery(SongController.filterableFields) where: Prisma.SongWhereInput,
@Query('include') include: string,
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Song>> {
@@ -147,16 +193,26 @@ export class SongController {
skip,
take,
where,
include: mapInclude(include, req, SongController.includableFields),
});
return new Plage(ret, req);
}
@Get(':id')
@ApiOperation({ description: "Get a specific song data"})
@ApiNotFoundResponse({ description: "Song not found"})
@ApiOkResponse({ type: _Song, description: "Requested song"})
async findOne(@Param('id', ParseIntPipe) id: number) {
const res = await this.songService.song({ id });
@ApiOperation({ description: 'Get a specific song data' })
@ApiNotFoundResponse({ description: 'Song not found' })
@ApiOkResponse({ type: _Song, description: 'Requested song' })
async findOne(
@Req() req: Request,
@Param('id', ParseIntPipe) id: number,
@Query('include') include: string,
) {
const res = await this.songService.song(
{
id,
},
mapInclude(include, req, SongController.includableFields),
);
if (res === null) throw new NotFoundException('Song not found');
return res;
@@ -164,9 +220,13 @@ export class SongController {
@Get(':id/history')
@HttpCode(200)
@UseGuards(JwtAuthGuard)
@ApiOperation({ description: "get the history of the connected user on a specific song"})
@ApiOkResponse({ type: SongHistoryResult, description: "Records of previous games of the user"})
@ApiOperation({
description: 'get the history of the connected user on a specific song',
})
@ApiOkResponse({
type: SongHistoryResult,
description: 'Records of previous games of the user',
})
@ApiUnauthorizedResponse({ description: 'Invalid token' })
async getHistory(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
return this.historyService.getForSong({

View File

@@ -9,7 +9,7 @@ export class SongService {
async songByArtist(data: number): Promise<Song[]> {
return this.prisma.song.findMany({
where: {
artistId: {equals: data},
artistId: { equals: data },
},
});
}
@@ -22,9 +22,11 @@ export class SongService {
async song(
songWhereUniqueInput: Prisma.SongWhereUniqueInput,
include?: Prisma.SongInclude,
): Promise<Song | null> {
return this.prisma.song.findUnique({
where: songWhereUniqueInput,
include,
});
}
@@ -34,14 +36,16 @@ export class SongService {
cursor?: Prisma.SongWhereUniqueInput;
where?: Prisma.SongWhereInput;
orderBy?: Prisma.SongOrderByWithRelationInput;
include?: Prisma.SongInclude;
}): Promise<Song[]> {
const { skip, take, cursor, where, orderBy } = params;
const { skip, take, cursor, where, orderBy, include } = params;
return this.prisma.song.findMany({
skip,
take,
cursor,
where,
orderBy,
include,
});
}

View File

@@ -1,4 +1,11 @@
import { Controller, Get, Post, Param, NotFoundException, Response } from '@nestjs/common';
import {
Controller,
Get,
Post,
Param,
NotFoundException,
Response,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { ApiNotFoundResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { User } from 'src/models/user';
@@ -22,7 +29,9 @@ export class UsersController {
}
@Get(':id/picture')
@ApiOkResponse({description: 'Return the profile picture of the requested user'})
@ApiOkResponse({
description: 'Return the profile picture of the requested user',
})
async getPicture(@Response() res: any, @Param('id') id: number) {
return await this.usersService.getProfilePicture(+id, res);
}

View File

@@ -12,9 +12,7 @@ import fetch from 'node-fetch';
@Injectable()
export class UsersService {
constructor(
private prisma: PrismaService,
) {}
constructor(private prisma: PrismaService) {}
async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
@@ -101,35 +99,21 @@ export class UsersService {
resp.body!.pipe(res);
}
async addLikedSong(
userId: number,
songId: number,
) {
return this.prisma.likedSongs.create(
{
data: { songId: songId, userId: userId }
}
)
async addLikedSong(userId: number, songId: number) {
return this.prisma.likedSongs.create({
data: { songId: songId, userId: userId },
});
}
async getLikedSongs(
userId: number,
) {
return this.prisma.likedSongs.findMany(
{
where: { userId: userId },
}
)
async getLikedSongs(userId: number) {
return this.prisma.likedSongs.findMany({
where: { userId: userId },
});
}
async removeLikedSong(
userId: number,
songId: number,
) {
return this.prisma.likedSongs.deleteMany(
{
where: { userId: userId, songId: songId },
}
)
async removeLikedSong(userId: number, songId: number) {
return this.prisma.likedSongs.deleteMany({
where: { userId: userId, songId: songId },
});
}
}

33
back/src/utils/include.ts Normal file
View File

@@ -0,0 +1,33 @@
import { Request } from 'express';
import { BadRequestException } from '@nestjs/common';
export type IncludeMap<IncludeType> = {
[key in keyof IncludeType]:
| boolean
| ((ctx: { user: { id: number; username: string } }) => IncludeType[key]);
};
export function mapInclude<IncludeType>(
include: string | undefined,
req: Request,
fields: IncludeMap<IncludeType>,
): IncludeType | undefined {
if (!include) return undefined;
const ret: IncludeType = {} as IncludeType;
for (const key of include.split(',')) {
const value =
typeof fields[key] === 'function'
? fields[key]({ user: req.user })
: fields[key];
if (value !== false && value !== undefined) ret[key] = value;
else {
throw new BadRequestException(
`Invalid include, ${key} is not valid. Valid includes are: ${Object.keys(
fields,
).join(', ')}.`,
);
}
}
return ret;
}

View File

@@ -3,6 +3,7 @@ Documentation Tests of the /song route.
... Ensures that the songs CRUD works corectly.
Resource ../rest.resource
Resource ../auth/auth.resource
*** Test Cases ***
@@ -133,5 +134,47 @@ Get midi file
Integer response status 201
GET /song/${res.body.id}/midi
Integer response status 200
#Output
# Output
[Teardown] DELETE /song/${res.body.id}
Find a song with artist
[Documentation] Create a song and find it with it's artist
&{res2}= POST /artist { "name": "Tghjmk"}
Output
Integer response status 201
&{res}= POST
... /song
... {"name": "Mama miaeyi", "artistId": ${res2.body.id}, "difficulties": {}, "midiPath": "/musics/Beethoven-125-4.midi", "musicXmlPath": "/musics/Beethoven-125-4.mxl"}
Output
Integer response status 201
&{get}= GET /song/${res.body.id}?include=artist
Output
Integer response status 200
Should Be Equal ${res2.body} ${get.body.artist}
[Teardown] Run Keywords DELETE /song/${res.body.id}
... AND DELETE /artist/${res2.body.id}
Find a song with artist and history
[Documentation] Create a song and find it with it's artist
${userID}= RegisterLogin wowusersfkj
&{res2}= POST /artist { "name": "Tghjmk"}
Output
Integer response status 201
&{res}= POST
... /song
... {"name": "Mama miaeyi", "artistId": ${res2.body.id}, "difficulties": {}, "midiPath": "/musics/Beethoven-125-4.midi", "musicXmlPath": "/musics/Beethoven-125-4.mxl"}
Output
Integer response status 201
&{res3}= POST
... /history
... { "songID": ${res.body.id}, "userID": ${userID}, "score": 12, "difficulties": {}, "info": {} }
Output
Integer response status 201
&{get}= GET /song/${res.body.id}?include=artist,SongHistory
Output
Integer response status 200
Should Be Equal ${res2.body} ${get.body.artist}
Should Be Equal ${res3.body} ${get.body.SongHistory[0]}
[Teardown] Run Keywords DELETE /auth/me
... AND DELETE /song/${res.body.id}
... AND DELETE /artist/${res2.body.id}

View File

@@ -70,13 +70,14 @@ services:
nginx:
image: nginx
environment:
- API_URL=http://back:3000
- SCOROMETER_URL=http://scorometer:6543
- FRONT_URL=http://front:19006
- API_URL=${API_URL:-http://back:3000}
- SCOROMETER_URL=${SCOROMETER_URL:-http://scorometer:6543}
- FRONT_URL=${FRONT_URL:-http://front:19006}
- PORT=4567
depends_on:
- back
- front
- scorometer
volumes:
- "./front/assets:/assets:ro"
- "./front/nginx.conf.template.dev:/etc/nginx/templates/default.conf.template:ro"

31
front/.gitignore vendored
View File

@@ -1,20 +1,35 @@
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
npm-debug.*
web-build/
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.orig.*
web-build/
*.apk
yarn.error*
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
yarn-error.log
# local env files
.env*.local
.idea/
.expo
# typescript
*.tsbuildinfo

View File

@@ -66,9 +66,7 @@ export class ValidationError extends Error {
export default class API {
public static readonly baseUrl =
process.env.NODE_ENV != 'development' && Platform.OS === 'web'
? '/api'
: 'https://nightly.chroma.octohub.app/api';
Platform.OS === 'web' ? '/api' : process.env.EXPO_PUBLIC_API_URL!;
public static async fetch(
params: FetchParams,
handle: Pick<Required<HandleParams>, 'raw'>

View File

@@ -4,20 +4,21 @@
FROM node:16-alpine as build
WORKDIR /app
# install expo cli
RUN yarn global add expo-cli@6.0.5
RUN yarn global add expo-cli
# add sharp-cli (^2.1.0) for faster image processing
RUN yarn global add sharp-cli@^2.1.0
RUN yarn global add sharp-cli
COPY package.json yarn.lock ./
RUN yarn install
RUN yarn install --immutable
RUN expo install
COPY . .
ARG API_URL
ENV API_URL=$API_URL
ENV EXPO_PUBLIC_API_URL=$API_URL
ARG SCORO_URL
ENV SCORO_URL=$SCORO_URL
ENV EXPO_PUBLIC_API_URL=$SCORO_URL
RUN yarn tsc && expo build:web
RUN yarn tsc && npx expo export:web
# Serve the app
FROM nginx:1.21-alpine

View File

@@ -96,7 +96,7 @@ const protectedRoutes = () =>
options: { title: 'Verify email', headerShown: false },
link: '/verify',
},
} as const);
}) as const;
const publicRoutes = () =>
({
@@ -115,11 +115,6 @@ const publicRoutes = () =>
options: { title: translate('signUpBtn'), headerShown: false },
link: '/signup',
},
Oops: {
component: ProfileErrorView,
options: { title: 'Oops', headerShown: false },
link: undefined,
},
Google: {
component: GoogleView,
options: { title: 'Google signin', headerShown: false },
@@ -135,7 +130,7 @@ const publicRoutes = () =>
options: { title: 'Password reset form', headerShown: false },
link: '/forgot_password',
},
} as const);
}) as const;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Route<Props = any> = {
@@ -156,19 +151,18 @@ type PrivateRoutesParams = RouteParams<ReturnType<typeof protectedRoutes>>;
type PublicRoutesParams = RouteParams<ReturnType<typeof publicRoutes>>;
type AppRouteParams = PrivateRoutesParams & PublicRoutesParams;
const Stack = createNativeStackNavigator<AppRouteParams & { Loading: never }>();
const Stack = createNativeStackNavigator<AppRouteParams & { Loading: never; Oops: never }>();
const RouteToScreen =
<T extends {}>(component: Route<T>['component']) =>
// eslint-disable-next-line react/display-name
(props: NativeStackScreenProps<T & ParamListBase>) =>
(
<>
{component({ ...props.route.params, route: props.route } as Parameters<
Route<T>['component']
>[0])}
</>
);
(props: NativeStackScreenProps<T & ParamListBase>) => (
<>
{component({ ...props.route.params, route: props.route } as Parameters<
Route<T>['component']
>[0])}
</>
);
const routesToScreens = (routes: Partial<Record<keyof AppRouteParams, Route>>) =>
Object.entries(routes).map(([name, route], routeIndex) => (
@@ -205,6 +199,8 @@ const routesToLinkingConfig = (
const ProfileErrorView = (props: { onTryAgain: () => void }) => {
const dispatch = useDispatch();
const navigation = useNavigation();
return (
<Center style={{ flexGrow: 1 }}>
<VStack space={3}>
@@ -213,7 +209,10 @@ const ProfileErrorView = (props: { onTryAgain: () => void }) => {
<Translate translationKey="tryAgain" />
</Button>
<TextButton
onPress={() => dispatch(unsetAccessToken())}
onPress={() => {
dispatch(unsetAccessToken());
navigation.navigate('Start');
}}
colorScheme="error"
variant="outline"
translate={{ translationKey: 'signOutBtn' }}
@@ -274,12 +273,15 @@ export const Router = () => {
>
<Stack.Navigator>
{authStatus == 'error' ? (
<Stack.Screen
name="Oops"
component={RouteToScreen(() => (
<ProfileErrorView onTryAgain={() => userProfile.refetch()} />
))}
/>
<>
<Stack.Screen
name="Oops"
component={RouteToScreen(() => (
<ProfileErrorView onTryAgain={() => userProfile.refetch()} />
))}
/>
{routesToScreens(publicRoutes())}
</>
) : (
routesToScreens(routes)
)}

15
front/android/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
# OSX
#
.DS_Store
# Android/IntelliJ
#
build/
.idea
.gradle
local.properties
*.iml
*.hprof
# Bundle artifacts
*.jsbundle

View File

@@ -0,0 +1,180 @@
apply plugin: "com.android.application"
apply plugin: "com.facebook.react"
def projectRoot = rootDir.getAbsoluteFile().getParentFile().getAbsolutePath()
/**
* This is the configuration block to customize your React Native Android app.
* By default you don't need to apply any configuration, just uncomment the lines you need.
*/
react {
entryFile = file(["node", "-e", "require('expo/scripts/resolveAppEntry')", projectRoot, "android", "absolute"].execute(null, rootDir).text.trim())
reactNativeDir = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
hermesCommand = new File(["node", "--print", "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsolutePath() + "/sdks/hermesc/%OS-BIN%/hermesc"
codegenDir = new File(["node", "--print", "require.resolve('@react-native/codegen/package.json')"].execute(null, rootDir).text.trim()).getParentFile().getAbsoluteFile()
// Use Expo CLI to bundle the app, this ensures the Metro config
// works correctly with Expo projects.
cliFile = new File(["node", "--print", "require.resolve('@expo/cli')"].execute(null, rootDir).text.trim())
bundleCommand = "export:embed"
/* Folders */
// The root of your project, i.e. where "package.json" lives. Default is '..'
// root = file("../")
// The folder where the react-native NPM package is. Default is ../node_modules/react-native
// reactNativeDir = file("../node_modules/react-native")
// The folder where the react-native Codegen package is. Default is ../node_modules/@react-native/codegen
// codegenDir = file("../node_modules/@react-native/codegen")
/* Variants */
// The list of variants to that are debuggable. For those we're going to
// skip the bundling of the JS bundle and the assets. By default is just 'debug'.
// If you add flavors like lite, prod, etc. you'll have to list your debuggableVariants.
// debuggableVariants = ["liteDebug", "prodDebug"]
/* Bundling */
// A list containing the node command and its flags. Default is just 'node'.
// nodeExecutableAndArgs = ["node"]
//
// The path to the CLI configuration file. Default is empty.
// bundleConfig = file(../rn-cli.config.js)
//
// The name of the generated asset file containing your JS bundle
// bundleAssetName = "MyApplication.android.bundle"
//
// The entry file for bundle generation. Default is 'index.android.js' or 'index.js'
// entryFile = file("../js/MyApplication.android.js")
//
// A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = []
/* Hermes Commands */
// The hermes compiler command to run. By default it is 'hermesc'
// hermesCommand = "$rootDir/my-custom-hermesc/bin/hermesc"
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
}
/**
* Set this to true to Run Proguard on Release builds to minify the Java bytecode.
*/
def enableProguardInReleaseBuilds = (findProperty('android.enableProguardInReleaseBuilds') ?: false).toBoolean()
/**
* The preferred build flavor of JavaScriptCore (JSC)
*
* For example, to use the international variant, you can use:
* `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
*
* The international variant includes ICU i18n library and necessary data
* allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
* give correct results when using with locales other than en-US. Note that
* this variant is about 6MiB larger per architecture than default.
*/
def jscFlavor = 'org.webkit:android-jsc:+'
android {
ndkVersion rootProject.ext.ndkVersion
compileSdkVersion rootProject.ext.compileSdkVersion
namespace 'build.apk'
defaultConfig {
applicationId 'build.apk'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0.0"
buildConfigField("boolean", "REACT_NATIVE_UNSTABLE_USE_RUNTIME_SCHEDULER_ALWAYS", (findProperty("reactNative.unstable_useRuntimeSchedulerAlways") ?: true).toString())
}
signingConfigs {
debug {
storeFile file('debug.keystore')
storePassword 'android'
keyAlias 'androiddebugkey'
keyPassword 'android'
}
}
buildTypes {
debug {
signingConfig signingConfigs.debug
}
release {
// Caution! In production, you need to generate your own keystore file.
// see https://reactnative.dev/docs/signed-apk-android.
signingConfig signingConfigs.debug
shrinkResources (findProperty('android.enableShrinkResourcesInReleaseBuilds')?.toBoolean() ?: false)
minifyEnabled enableProguardInReleaseBuilds
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
}
}
}
// Apply static values from `gradle.properties` to the `android.packagingOptions`
// Accepts values in comma delimited lists, example:
// android.packagingOptions.pickFirsts=/LICENSE,**/picasa.ini
["pickFirsts", "excludes", "merges", "doNotStrip"].each { prop ->
// Split option: 'foo,bar' -> ['foo', 'bar']
def options = (findProperty("android.packagingOptions.$prop") ?: "").split(",");
// Trim all elements in place.
for (i in 0..<options.size()) options[i] = options[i].trim();
// `[] - ""` is essentially `[""].filter(Boolean)` removing all empty strings.
options -= ""
if (options.length > 0) {
println "android.packagingOptions.$prop += $options ($options.length)"
// Ex: android.packagingOptions.pickFirsts += '**/SCCS/**'
options.each {
android.packagingOptions[prop] += it
}
}
}
dependencies {
// The version of react-native is set by the React Native Gradle Plugin
implementation("com.facebook.react:react-android")
def isGifEnabled = (findProperty('expo.gif.enabled') ?: "") == "true";
def isWebpEnabled = (findProperty('expo.webp.enabled') ?: "") == "true";
def isWebpAnimatedEnabled = (findProperty('expo.webp.animated') ?: "") == "true";
def frescoVersion = rootProject.ext.frescoVersion
// If your app supports Android versions before Ice Cream Sandwich (API level 14)
if (isGifEnabled || isWebpEnabled) {
implementation("com.facebook.fresco:fresco:${frescoVersion}")
implementation("com.facebook.fresco:imagepipeline-okhttp3:${frescoVersion}")
}
if (isGifEnabled) {
// For animated gif support
implementation("com.facebook.fresco:animated-gif:${frescoVersion}")
}
if (isWebpEnabled) {
// For webp support
implementation("com.facebook.fresco:webpsupport:${frescoVersion}")
if (isWebpAnimatedEnabled) {
// Animated webp support
implementation("com.facebook.fresco:animated-webp:${frescoVersion}")
}
}
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
exclude group:'com.squareup.okhttp3', module:'okhttp'
}
debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}")
if (hermesEnabled.toBoolean()) {
implementation("com.facebook.react:hermes-android")
} else {
implementation jscFlavor
}
}
apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
applyNativeModulesAppBuildGradle(project)

Binary file not shown.

14
front/android/app/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,14 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# react-native-reanimated
-keep class com.swmansion.reanimated.** { *; }
-keep class com.facebook.react.turbomodule.** { *; }
# Add any project specific keep options here:

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application android:usesCleartextTraffic="true" tools:targetApi="28" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:usesCleartextTraffic" />
</manifest>

View File

@@ -0,0 +1,33 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https"/>
</intent>
</queries>
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:allowBackup="true" android:theme="@style/AppTheme">
<meta-data android:name="expo.modules.updates.ENABLED" android:value="false"/>
<meta-data android:name="expo.modules.updates.EXPO_SDK_VERSION" android:value="49.0.0"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_CHECK_ON_LAUNCH" android:value="ALWAYS"/>
<meta-data android:name="expo.modules.updates.EXPO_UPDATES_LAUNCH_WAIT_MS" android:value="0"/>
<activity android:name=".MainActivity" android:label="@string/app_name" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="build.apk"/>
</intent-filter>
</activity>
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false"/>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2014 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<inset xmlns:android="http://schemas.android.com/apk/res/android"
android:insetLeft="@dimen/abc_edit_text_inset_horizontal_material"
android:insetRight="@dimen/abc_edit_text_inset_horizontal_material"
android:insetTop="@dimen/abc_edit_text_inset_top_material"
android:insetBottom="@dimen/abc_edit_text_inset_bottom_material">
<selector>
<!--
This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
<item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
-->
<item android:state_enabled="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
<item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
</selector>
</inset>

View File

@@ -0,0 +1,3 @@
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/splashscreen_background"/>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -0,0 +1 @@
<resources/>

View File

@@ -0,0 +1,6 @@
<resources>
<color name="splashscreen_background">#ffffff</color>
<color name="iconBackground">#FFFFFF</color>
<color name="colorPrimary">#023c69</color>
<color name="colorPrimaryDark">#ffffff</color>
</resources>

View File

@@ -0,0 +1,5 @@
<resources>
<string name="app_name">Chromacase</string>
<string name="expo_splash_screen_resize_mode" translatable="false">cover</string>
<string name="expo_splash_screen_status_bar_translucent" translatable="false">false</string>
</resources>

View File

@@ -0,0 +1,17 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<item name="android:textColor">@android:color/black</item>
<item name="android:editTextStyle">@style/ResetEditText</item>
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
</style>
<style name="ResetEditText" parent="@android:style/Widget.EditText">
<item name="android:padding">0dp</item>
<item name="android:textColorHint">#c8c8c8</item>
<item name="android:textColor">@android:color/black</item>
</style>
<style name="Theme.App.SplashScreen" parent="AppTheme">
<item name="android:windowBackground">@drawable/splashscreen</item>
</style>
</resources>

View File

@@ -0,0 +1,40 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext {
buildToolsVersion = findProperty('android.buildToolsVersion') ?: '33.0.0'
minSdkVersion = Integer.parseInt(findProperty('android.minSdkVersion') ?: '21')
compileSdkVersion = Integer.parseInt(findProperty('android.compileSdkVersion') ?: '33')
targetSdkVersion = Integer.parseInt(findProperty('android.targetSdkVersion') ?: '33')
kotlinVersion = findProperty('android.kotlinVersion') ?: '1.8.10'
frescoVersion = findProperty('expo.frescoVersion') ?: '2.5.0'
// We use NDK 23 which has both M1 support and is the side-by-side NDK version from AGP.
ndkVersion = "23.1.7779620"
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath('com.android.tools.build:gradle:7.4.2')
classpath('com.facebook.react:react-native-gradle-plugin')
}
}
allprojects {
repositories {
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url(new File(['node', '--print', "require.resolve('react-native/package.json')"].execute(null, rootDir).text.trim(), '../android'))
}
maven {
// Android JSC is installed from npm
url(new File(['node', '--print', "require.resolve('jsc-android/package.json')"].execute(null, rootDir).text.trim(), '../dist'))
}
google()
mavenCentral()
maven { url 'https://www.jitpack.io' }
}
}

View File

@@ -0,0 +1,56 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
# Default value: -Xmx512m -XX:MaxMetaspaceSize=256m
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.182.0
# Use this property to specify which architecture you want to build.
# You can also override it from the CLI using
# ./gradlew <task> -PreactNativeArchitectures=x86_64
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# Use this property to enable support to the new architecture.
# This will allow you to use TurboModules and the Fabric render in
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
hermesEnabled=true
# Enable GIF support in React Native images (~200 B increase)
expo.gif.enabled=true
# Enable webp support in React Native images (~85 KB increase)
expo.webp.enabled=true
# Enable animated webp support (~3.4 MB increase)
# Disabled by default because iOS doesn't support animated webp
expo.webp.animated=false
# Enable network inspector
EX_DEV_CLIENT_NETWORK_INSPECTOR=true

Binary file not shown.

View File

@@ -0,0 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.1-all.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

240
front/android/gradlew vendored Executable file
View File

@@ -0,0 +1,240 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

91
front/android/gradlew.bat vendored Normal file
View File

@@ -0,0 +1,91 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@@ -0,0 +1,10 @@
rootProject.name = 'Chromacase'
apply from: new File(["node", "--print", "require.resolve('expo/package.json')"].execute(null, rootDir).text.trim(), "../scripts/autolinking.gradle");
useExpoModules()
apply from: new File(["node", "--print", "require.resolve('@react-native-community/cli-platform-android/package.json')"].execute(null, rootDir).text.trim(), "../native_modules.gradle");
applyNativeModulesSettingsGradle(settings)
include ':app'
includeBuild(new File(["node", "--print", "require.resolve('@react-native/gradle-plugin/package.json')"].execute(null, rootDir).text.trim()).getParentFile())

View File

@@ -1,33 +0,0 @@
module.exports = {
name: 'Chromacase',
slug: 'Chromacase',
version: '1.0.0',
orientation: 'portrait',
icon: './assets/icon.png',
userInterfaceStyle: 'light',
splash: {
image: './assets/splash.png',
resizeMode: 'contain',
backgroundColor: '#ffffff',
},
updates: {
fallbackToCacheTimeout: 0,
},
assetBundlePatterns: ['**/*'],
ios: {
supportsTablet: true,
},
android: {
package: 'build.apk',
},
web: {
favicon: './assets/favicon.png',
},
extra: {
apiUrl: process.env.API_URL,
scoroUrl: process.env.SCORO_URL,
eas: {
projectId: 'dade8e5e-3e2c-49f7-98c5-cf8834c7ebb2',
},
},
};

View File

@@ -11,15 +11,15 @@
"resizeMode": "cover",
"backgroundColor": "#ffffff"
},
"updates": {
"fallbackToCacheTimeout": 0
},
"assetBundlePatterns": ["**/*"],
"ios": {
"supportsTablet": true
},
"android": {
"package": "build.apk"
"adaptiveIcon": {
"foregroundImage": "./assets/icon.png",
"backgroundColor": "#ffffff"
}
},
"web": {
"favicon": "./assets/favicon.png"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 2.3 MiB

View File

@@ -2,11 +2,24 @@ module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['@babel/plugin-proposal-export-namespace-from', 'react-native-reanimated/plugin'],
env: {
production: {
plugins: ['react-native-paper/babel'],
},
},
plugins: [
[
'module:react-native-dotenv',
{
moduleName: '@env',
path: '.env',
blacklist: null,
whitelist: null,
safe: false,
allowUndefined: true,
},
],
],
// plugins: ['@babel/plugin-proposal-export-namespace-from', 'react-native-reanimated/plugin'],
// env: {
// production: {
// plugins: ['react-native-paper/babel'],
// },
// },
};
};

View File

@@ -91,11 +91,11 @@ const BigActionButton = ({
<Box
style={{
position: 'absolute',
left: '0',
left: 0,
width: '100%',
height: '100%',
backgroundColor: isDark ? 'black' : 'white',
padding: '10px',
padding: 10,
}}
>
<Row>

View File

@@ -0,0 +1,35 @@
import { HStack, Icon, Text } from 'native-base';
import { MaterialIcons } from '@expo/vector-icons';
type DurationComponentProps = {
length: number | undefined;
};
const DurationComponent = ({ length }: DurationComponentProps) => {
const minutes = Math.floor((length ?? 0) / 60);
const seconds = Math.round((length ?? 0) - minutes * 60);
return (
<HStack space={3}>
<Icon
as={MaterialIcons}
name="timer"
size={'20px'}
color="coolGray.800"
_dark={{
color: 'warmGray.50',
}}
/>
<Text
style={{
flexShrink: 0,
}}
fontSize={'16px'}
>
{length ? `${minutes}'${seconds}` : "--'--"}
</Text>
</HStack>
);
};
export default DurationComponent;

View File

@@ -4,6 +4,7 @@ import TextButton from './TextButton';
import { LikedSongWithDetails } from '../models/LikedSong';
import { MaterialIcons } from '@expo/vector-icons';
import API from '../API';
import DurationComponent from './DurationComponent';
type FavSongRowProps = {
FavSong: LikedSongWithDetails; // TODO: remove Song
@@ -54,6 +55,7 @@ const FavSongRow = ({ FavSong, onPress }: FavSongRowProps) => {
>
{FavSong.addedDate.toLocaleDateString()}
</Text>
<DurationComponent length={FavSong.details.details.length} />
</HStack>
<IconButton
colorScheme="primary"

View File

@@ -9,6 +9,7 @@ import {
getElementRangeNode,
} from './ElementTypes';
import { ArrowDown2 } from 'iconsax-react-native';
import { useWindowDimensions } from 'react-native';
type RawElementProps = {
element: ElementProps;
@@ -18,6 +19,7 @@ export const RawElement = ({ element }: RawElementProps) => {
const { title, icon, type, helperText, description, disabled, data } = element;
const screenSize = useBreakpointValue({ base: 'small', md: 'big' });
const isSmallScreen = screenSize === 'small';
const { width: screenWidth } = useWindowDimensions();
return (
<Column
style={{
@@ -97,7 +99,9 @@ export const RawElement = ({ element }: RawElementProps) => {
<Popover.Content
accessibilityLabel={`Additionnal information for ${title}`}
style={{
maxWidth: isSmallScreen ? '90vw' : '20vw',
maxWidth: isSmallScreen
? 0.9 * screenWidth
: 0.2 * screenWidth,
}}
>
<Popover.Arrow />

View File

@@ -1,3 +1,4 @@
// @ts-expect-error Who does tests anyway?
import { ComponentStory, ComponentMeta } from '@storybook/react';
import Loading from './Loading';

View File

@@ -3,6 +3,7 @@ import Song, { SongWithArtist } from '../models/Song';
import RowCustom from './RowCustom';
import TextButton from './TextButton';
import { MaterialIcons } from '@expo/vector-icons';
import DurationComponent from './DurationComponent';
type SongRowProps = {
song: Song | SongWithArtist; // TODO: remove Song
@@ -55,6 +56,8 @@ const SongRow = ({ song, onPress, handleLike, isLiked }: SongRowProps) => {
>
{song.artistId ?? 'artist'}
</Text>
{/* <DurationInfo length={song.details.length} /> */}
<DurationComponent length={song.details.length} />
</HStack>
<IconButton
colorScheme="rose"

View File

@@ -7,7 +7,7 @@ import { translate } from '../../i18n/i18n';
import API from '../../API';
import SeparatorBase from './SeparatorBase';
import LinkBase from './LinkBase';
import ImageBanner from '../../assets/banner.jpg';
import { useAssets } from 'expo-asset';
interface ScaffoldAuthProps {
title: string;
@@ -25,6 +25,8 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
link,
}) => {
const layout = useWindowDimensions();
// eslint-disable-next-line @typescript-eslint/no-var-requires
const [banner] = useAssets(require('../../assets/banner.jpg'));
return (
<Flex
@@ -83,7 +85,7 @@ const ScaffoldAuth: FunctionComponent<ScaffoldAuthProps> = ({
{layout.width > 650 ? (
<View style={{ width: '50%', height: '100%', padding: 16 }}>
<Image
source={ImageBanner}
source={{ uri: banner?.at(0)?.uri }}
alt="banner page"
style={{ width: '100%', height: '100%', borderRadius: 8 }}
/>

View File

@@ -92,7 +92,6 @@ const SongCardInfo = (props: SongCardInfoProps) => {
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
// @ts-expect-error gap isn't yet supported by react native
gap: 5,
paddingHorizontal: 10,
}}

View File

@@ -96,13 +96,13 @@ const TabNavigation = () => {
setActiveTabID={setActiveTab}
>
<View
// @ts-expect-error Raw CSS
style={{
width: 'calc(100% - 5)',
height: '100%',
backgroundColor: 'rgba(16, 16, 20, 0.50)',
borderRadius: 12,
margin: 5,
// @ts-expect-error backDropFilter isn't yet supported by react native
backDropFilter: 'blur(2px)',
padding: 15,
}}
@@ -119,6 +119,7 @@ const TabNavigation = () => {
setIsCollapsed={setIsDesktopCollapsed}
>
<View
// @ts-expect-error Raw CSS
style={{
width: 'calc(100% - 10)',
height: '100%',
@@ -126,7 +127,6 @@ const TabNavigation = () => {
borderRadius: 12,
marginVertical: 10,
marginRight: 10,
// @ts-expect-error backDropFilter isn't yet supported by react native
backDropFilter: 'blur(2px)',
padding: 20,
}}

View File

@@ -20,57 +20,57 @@ const TabNavigationButton = (props: TabNavigationButtonProps) => {
width: '100%',
}}
>
{({ isPressed, isHovered }) => (
<View
style={{
display: 'flex',
flexDirection: 'row',
alignSelf: 'stretch',
alignItems: 'center',
justifyContent: 'flex-start',
padding: '10px',
borderRadius: 8,
flexGrow: 0,
// @ts-expect-error BoxShadow is not in the types but I want it this may be a legitimate error on my part
boxShadow: (() => {
if (isHovered) {
return '0px 0px 16px 0px rgba(0, 0, 0, 0.25)';
} else if (props.isActive) {
return '0px 0px 8px 0px rgba(0, 0, 0, 0.25)';
} else {
return undefined;
}
})(),
backdropFilter: 'blur(2px)',
backgroundColor: (() => {
if (isPressed) {
return 'rgba(0, 0, 0, 0.1)';
} else if (isHovered) {
return 'rgba(231, 231, 232, 0.2)';
} else if (props.isActive) {
return 'rgba(16, 16, 20, 0.5)';
} else {
return 'transparent';
}
})(),
}}
>
{props.icon && (
<View
style={{
marginRight: props.isCollapsed ? undefined : '10px',
}}
>
{props.icon}
</View>
)}
{!props.isCollapsed && (
<Text numberOfLines={1} selectable={false}>
{props.label}
</Text>
)}
</View>
)}
{({ isPressed, isHovered }) => {
let boxShadow: string | undefined = undefined;
if (isHovered) {
boxShadow = '0px 0px 16px 0px rgba(0, 0, 0, 0.25)';
} else if (props.isActive) {
boxShadow = '0px 0px 8px 0px rgba(0, 0, 0, 0.25)';
}
return (
<View
style={{
display: 'flex',
flexDirection: 'row',
alignSelf: 'stretch',
alignItems: 'center',
justifyContent: 'flex-start',
padding: 10,
borderRadius: 8,
flexGrow: 0,
// @ts-expect-error boxShadow isn't yet supported by react native
boxShadow: boxShadow,
backdropFilter: 'blur(2px)',
backgroundColor: (() => {
if (isPressed) {
return 'rgba(0, 0, 0, 0.1)';
} else if (isHovered) {
return 'rgba(231, 231, 232, 0.2)';
} else if (props.isActive) {
return 'rgba(16, 16, 20, 0.5)';
} else {
return 'transparent';
}
})(),
}}
>
{props.icon && (
<View
style={{
marginRight: props.isCollapsed ? undefined : 10,
}}
>
{props.icon}
</View>
)}
{!props.isCollapsed && (
<Text numberOfLines={1} selectable={false}>
{props.label}
</Text>
)}
</View>
);
}}
</Pressable>
);
};

View File

@@ -50,16 +50,16 @@ const TabNavigationDesktop = (props: TabNavigationDesktopProps) => {
alignItems: 'center',
justifyContent: 'flex-start',
flexShrink: 0,
padding: '10px',
padding: 10,
}}
>
<Image
source={{ uri: icon?.at(0)?.uri }}
style={{
aspectRatio: 1,
width: '40px',
width: 40,
height: 'auto',
marginRight: '10px',
marginRight: 10,
}}
/>
<Text fontSize={'2xl'} selectable={false}>
@@ -70,9 +70,9 @@ const TabNavigationDesktop = (props: TabNavigationDesktopProps) => {
<View
style={{
display: 'flex',
width: '300px',
width: 300,
height: 'auto',
padding: '32px',
padding: 32,
flexDirection: 'column',
justifyContent: 'space-between',
alignItems: 'flex-start',
@@ -82,8 +82,7 @@ const TabNavigationDesktop = (props: TabNavigationDesktopProps) => {
<TabNavigationList
style={{
flexShrink: 0,
// @ts-expect-error gap is not in the types because we have an old version of react-native
gap: '20px',
gap: 20,
}}
>
{buttons.map((button, index) => (
@@ -104,8 +103,8 @@ const TabNavigationDesktop = (props: TabNavigationDesktopProps) => {
<Text
bold
style={{
paddingHorizontal: '16px',
paddingVertical: '10px',
paddingHorizontal: 16,
paddingVertical: 10,
fontSize: 20,
}}
>
@@ -114,8 +113,8 @@ const TabNavigationDesktop = (props: TabNavigationDesktopProps) => {
{songHistory.length === 0 && (
<Text
style={{
paddingHorizontal: '16px',
paddingVertical: '10px',
paddingHorizontal: 16,
paddingVertical: 10,
}}
>
No songs played yet
@@ -133,8 +132,8 @@ const TabNavigationDesktop = (props: TabNavigationDesktopProps) => {
<View
key={'tab-navigation-other-' + index}
style={{
paddingHorizontal: '16px',
paddingVertical: '10px',
paddingHorizontal: 16,
paddingVertical: 10,
}}
>
<Text numberOfLines={1}>{histoItem.name}</Text>
@@ -144,8 +143,7 @@ const TabNavigationDesktop = (props: TabNavigationDesktopProps) => {
<Divider />
<TabNavigationList
style={{
// @ts-expect-error gap is not in the types because we have an old version of react-native
gap: '20px',
gap: 20,
}}
>
{([props.tabs.find((t) => t.id === 'settings')] as NaviTab[]).map(
@@ -166,6 +164,7 @@ const TabNavigationDesktop = (props: TabNavigationDesktopProps) => {
</View>
</View>
<ScrollView
// @ts-expect-error Raw CSS
style={{
height: '100%',
width: 'calc(100% - 300px)',

View File

@@ -15,8 +15,7 @@ const TabNavigationList = (props: TabNavigationListProps) => {
alignItems: 'flex-start',
alignSelf: 'stretch',
flexDirection: 'column',
// @ts-expect-error gap is not in the types because we have an old version of react-native
gap: '8px',
gap: 8,
},
props.style,
]}

View File

@@ -22,8 +22,8 @@ const TabNavigationPhone = (props: TabNavigationPhoneProps) => {
>
<View
style={{
padding: '16px',
height: '90px',
padding: 16,
height: 90,
width: '100%',
}}
>
@@ -31,7 +31,7 @@ const TabNavigationPhone = (props: TabNavigationPhoneProps) => {
<View
style={{
display: 'flex',
padding: '8px',
padding: 8,
justifyContent: 'space-evenly',
flexDirection: 'row',
alignItems: 'center',
@@ -56,6 +56,7 @@ const TabNavigationPhone = (props: TabNavigationPhoneProps) => {
</Center>
</View>
<ScrollView
// @ts-expect-error Raw CSS
style={{
width: '100%',
height: 'calc(100% - 90px)',

Some files were not shown because too many files have changed in this diff Show More