24 Commits

Author SHA1 Message Date
GitBluub
10f033fe78 wip: password reset routes 2023-09-15 15:11:14 +02:00
869b2e696f Update .env.example 2023-09-13 17:32:10 +02:00
050c970e7e Add a button to resend verified mail 2023-09-13 17:30:34 +02:00
5c83235cba Add verified badge and page on the front 2023-09-13 17:25:01 +02:00
3b2ca9963b Use a fixed python version for the scorometer 2023-09-11 16:13:28 +02:00
0a08193418 Send mails on account creation 2023-09-07 16:58:18 +02:00
Clément Le Bihan
e0f2674811 fix pr 2023-09-06 15:09:54 +02:00
Clément Le Bihan
b84ee11f45 Fix de arthur 2023-09-06 15:09:54 +02:00
Clément Le Bihan
a2494ce498 prettied 2023-09-06 15:09:54 +02:00
Clément Le Bihan
b76d496034 fix ts issues 2 2023-09-06 15:09:54 +02:00
Clément Le Bihan
a81d3ee34d fixed ts type issue 2023-09-06 15:09:54 +02:00
Clément Le Bihan
85473ae492 Removed old commented react useState 2023-09-06 15:09:54 +02:00
Clément Le Bihan
9655e986ff Removed old code from HomeView and auto format some files 2023-09-06 15:09:54 +02:00
Clément Le Bihan
101ea8498b removing old code commented and unused dependancies 2023-09-06 15:09:54 +02:00
Clément Le Bihan
7d33f85cbc Cleanup parition view 2023-09-06 15:09:54 +02:00
Clément Le Bihan
66d792715e Removed Parition context declaration/init 2023-09-06 15:09:54 +02:00
Clément Le Bihan
40581f4a45 Removed the timestamp partition context to reuse normal props clean up console logs and now displaying a toast to tell is the scorometer crashed 2023-09-06 15:09:54 +02:00
Clément Le Bihan
2ca3fcb81a reactivating websocket connection but error view appear when it shouldn't 2023-09-06 15:09:54 +02:00
Clément Le Bihan
30fcacbec6 Now using redux to not create sound player every time the phaser is also implicitely cached 2023-09-06 15:09:54 +02:00
Clément Le Bihan
7c3289ccec now phasercanvas makes sounds used the same stack as previously and ram issue spotted 2023-09-06 15:09:54 +02:00
Clément Le Bihan
7438986bcd Cursor is controlled by partition timestamps provided by playview and can thus be paused and onEndReached is now called 2023-09-06 15:09:54 +02:00
Clément Le Bihan
3ac017a5f0 Cursor with cam follow is moving to correct notes, timing is fake 2023-09-06 15:09:54 +02:00
Clément Le Bihan
8e5cc1bc44 Added sliding to the partition but some issues 2023-09-06 15:09:54 +02:00
Clément Le Bihan
125a7faf02 early Experiment working 2023-09-06 15:09:54 +02:00
30 changed files with 5376 additions and 510 deletions

View File

@@ -10,3 +10,5 @@ SCORO_URL=ws://localhost:6543
GOOGLE_CLIENT_ID=toto
GOOGLE_SECRET=tata
GOOGLE_CALLBACK_URL=http://localhost:19006/logged/google
SMTP_TRANSPORT=
MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>'

5
.envrc
View File

@@ -1,4 +1 @@
if ! has nix_direnv_version || ! nix_direnv_version 2.2.1; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/2.2.1/direnvrc" "sha256-zelF0vLbEl5uaqrfIzbgNzJWGmLzCmYAkInj/LNxvKs="
fi
use flake
use nix

4994
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": {
"@nestjs-modules/mailer": "^1.9.1",
"@nestjs/common": "^10.1.0",
"@nestjs/config": "^3.0.0",
"@nestjs/core": "^10.1.0",
@@ -37,6 +38,7 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"node-fetch": "^2.6.12",
"nodemailer": "^6.9.5",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0",
@@ -53,6 +55,7 @@
"@types/jest": "29.5.3",
"@types/multer": "^1.4.7",
"@types/node": "^20.4.4",
"@types/nodemailer": "^6.4.9",
"@types/passport-google-oauth20": "^2.0.11",
"@types/supertest": "^2.0.12",
"@typescript-eslint/eslint-plugin": "^6.1.0",

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "emailVerified" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -14,6 +14,7 @@ model User {
username String @unique
password String?
email String
emailVerified Boolean @default(false)
googleID String? @unique
isGuest Boolean @default(false)
partyPlayed Int @default(0)

View File

@@ -14,6 +14,7 @@ import { ArtistModule } from './artist/artist.module';
import { AlbumModule } from './album/album.module';
import { SearchModule } from './search/search.module';
import { HistoryModule } from './history/history.module';
import { MailerModule } from '@nestjs-modules/mailer';
@Module({
imports: [
@@ -28,6 +29,12 @@ import { HistoryModule } from './history/history.module';
SearchModule,
SettingsModule,
HistoryModule,
MailerModule.forRoot({
transport: process.env.SMTP_TRANSPORT,
defaults: {
from: process.env.MAIL_AUTHOR,
},
}),
],
controllers: [AppController],
providers: [AppService, PrismaService, ArtistService],

View File

@@ -18,6 +18,7 @@ import {
HttpStatus,
ParseFilePipeBuilder,
Response,
Query,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';
@@ -71,12 +72,45 @@ export class AuthController {
try {
const user = await this.usersService.createUser(registerDto);
await this.settingsService.createUserSetting(user.id);
await this.authService.sendVerifyMail(user);
} catch (e) {
console.error(e);
throw new BadRequestException();
}
}
@HttpCode(200)
@UseGuards(JwtAuthGuard)
@Put('reset')
async password_reset(@Request() req: any, @Query('token') token: string): Promise<void> {
if (await this.authService.resetPassword(req.user.id, token))
return;
throw new BadRequestException("Invalid token. Expired or invalid.");
}
@HttpCode(200)
@UseGuards(JwtAuthGuard)
@Put('send-reset')
async send_reset(@Request() req: any): Promise<void> {
await this.authService.sendResetMail(req.user);
}
@HttpCode(200)
@UseGuards(JwtAuthGuard)
@Put('verify')
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.");
}
@HttpCode(200)
@UseGuards(JwtAuthGuard)
@Put('reverify')
async reverify(@Request() req: any): Promise<void> {
await this.authService.sendVerifyMail(req.user);
}
@ApiBody({ type: LoginDto })
@HttpCode(200)
@UseGuards(LocalAuthGuard)
@@ -121,7 +155,7 @@ export class AuthController {
)
file: Express.Multer.File,
) {
const path = `/data/${req.user.id}.jpg`
const path = `/data/${req.user.id}.jpg`;
writeFile(path, file.buffer, (err) => {
if (err) throw err;
});

View File

@@ -1,13 +1,16 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { UsersService } from '../users/users.service';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcryptjs';
import PayloadInterface from './interface/payload.interface';
import { User } from 'src/models/user';
import { MailerService } from '@nestjs-modules/mailer';
@Injectable()
export class AuthService {
constructor(
private userService: UsersService,
private jwtService: JwtService,
private emailService: MailerService,
) {}
async validateUser(
@@ -31,4 +34,33 @@ export class AuthService {
access_token,
};
}
async sendVerifyMail(user: User) {
const token = await this.jwtService.signAsync(
{
userId: user.id,
},
{ expiresIn: '10h' },
);
await this.emailService.sendMail({
to: user.email,
from: 'chromacase@octohub.app',
subject: 'Mail verification for Chromacase',
html: `To verify your mail, please click on this <a href="{${process.env.PUBLIC_URL}/verify?token=${token}">link</a>.`,
});
}
async verifyMail(userId: number, token: string): Promise<boolean> {
try {
await this.jwtService.verifyAsync(token);
} catch(e) {
console.log("Verify mail token failure", e);
return false;
}
await this.userService.updateUser({
where: { id: userId },
data: { emailVerified: true },
});
return true;
}
}

View File

@@ -1,8 +1,6 @@
import {
Injectable,
InternalServerErrorException,
NotFoundException,
StreamableFile,
} from '@nestjs/common';
import { User, Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
@@ -13,7 +11,9 @@ import fetch from 'node-fetch';
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}
constructor(
private prisma: PrismaService,
) {}
async user(
userWhereUniqueInput: Prisma.UserWhereUniqueInput,
@@ -95,8 +95,7 @@ export class UsersService {
const resp = await fetch(
`https://www.gravatar.com/avatar/${hash}.jpg?d=404&s=200`,
);
for (const [k, v] of resp.headers)
resp.headers.set(k, v);
for (const [k, v] of resp.headers) resp.headers.set(k, v);
resp.body!.pipe(res);
}
}

43
flake.lock generated
View File

@@ -1,43 +0,0 @@
{
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1665573177,
"narHash": "sha256-Arkrf3zmi3lXYpbSe9H+HQxswQ6jxsAmeQVq5Sr/OZc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d2afb051ffd904af5a825f58abee3c63b148c5f2",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "master",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

@@ -1,31 +0,0 @@
{
description = "A prisma test project";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/master";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system};
in {
devShell = pkgs.mkShell {
nativeBuildInputs = [ pkgs.bashInteractive ];
buildInputs = with pkgs; [
nodePackages.prisma
nodePackages."@nestjs/cli"
nodePackages.npm
nodejs-slim
yarn
python3
pkg-config
];
shellHook = with pkgs; ''
export PRISMA_MIGRATION_ENGINE_BINARY="${prisma-engines}/bin/migration-engine"
export PRISMA_QUERY_ENGINE_BINARY="${prisma-engines}/bin/query-engine"
export PRISMA_QUERY_ENGINE_LIBRARY="${prisma-engines}/lib/libquery_engine.node"
export PRISMA_INTROSPECTION_ENGINE_BINARY="${prisma-engines}/bin/introspection-engine"
export PRISMA_FMT_BINARY="${prisma-engines}/bin/prisma-fmt"
export DATABASE_URL=postgresql://user:eip@localhost:5432/chromacase
'';
};
});
}

View File

@@ -29,6 +29,7 @@ import { unsetAccessToken } from './state/UserSlice';
import TextButton from './components/TextButton';
import ErrorView from './views/ErrorView';
import GoogleView from './views/GoogleView';
import VerifiedView from './views/VerifiedView';
// Util function to hide route props in URL
const removeMe = () => '';
@@ -75,6 +76,11 @@ const protectedRoutes = () =>
link: undefined,
},
User: { component: ProfileView, options: { title: translate('user') }, link: '/user' },
Verified: {
component: VerifiedView,
options: { title: 'Verify email', headerShown: false },
link: '/verify',
},
} as const);
const publicRoutes = () =>

View File

@@ -0,0 +1,60 @@
import * as React from 'react';
import PartitionView from './PartitionView';
import PhaserCanvas from './PartitionVisualizer/PhaserCanvas';
import { PianoCursorPosition } from './PartitionVisualizer/PhaserCanvas';
type PartitionCoordProps = {
// The Buffer of the MusicXML file retreived from the API
file: string;
onPartitionReady: () => void;
onEndReached: () => void;
onResume: () => void;
onPause: () => void;
// Timestamp of the play session, in milisecond
timestamp: number;
};
const PartitionCoord = ({
file,
onPartitionReady,
onEndReached,
onPause,
onResume,
timestamp,
}: PartitionCoordProps) => {
const [partitionData, setPartitionData] = React.useState<
[string, PianoCursorPosition[]] | null
>(null);
return (
<>
{!partitionData && (
<PartitionView
file={file}
onPartitionReady={(base64data, a) => {
setPartitionData([base64data, a]);
onPartitionReady();
}}
onEndReached={() => {
console.log('osmd end reached');
}}
timestamp={timestamp}
/>
)}
{partitionData && (
<PhaserCanvas
partitionB64={partitionData?.[0]}
cursorPositions={partitionData?.[1]}
timestamp={timestamp}
onPause={onPause}
onResume={onResume}
onEndReached={() => {
onEndReached();
}}
/>
)}
</>
);
};
export default PartitionCoord;

View File

@@ -1,7 +1,7 @@
/* eslint-disable no-mixed-spaces-and-tabs */
// Inspired from OSMD example project
// https://github.com/opensheetmusicdisplay/react-opensheetmusicdisplay/blob/master/src/lib/OpenSheetMusicDisplay.jsx
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import {
CursorType,
Fraction,
@@ -10,29 +10,23 @@ import {
Note,
} from 'opensheetmusicdisplay';
import useColorScheme from '../hooks/colorScheme';
import { useWindowDimensions } from 'react-native';
import SoundFont from 'soundfont-player';
import * as SAC from 'standardized-audio-context';
import { PianoCursorPosition } from './PartitionVisualizer/PhaserCanvas';
type PartitionViewProps = {
// The Buffer of the MusicXML file retreived from the API
file: string;
onPartitionReady: () => void;
onPartitionReady: (base64data: string, cursorInfos: PianoCursorPosition[]) => void;
onEndReached: () => void;
// Timestamp of the play session, in milisecond
timestamp: number;
};
const PartitionView = (props: PartitionViewProps) => {
const [osmd, setOsmd] = useState<OSMD>();
const [soundPlayer, setSoundPlayer] = useState<SoundFont.Player>();
const audioContext = new SAC.AudioContext();
const [wholeNoteLength, setWholeNoteLength] = useState(0); // Length of Whole note, in ms (?)
const colorScheme = useColorScheme();
const dimensions = useWindowDimensions();
const OSMD_DIV_ID = 'osmd-div';
const options: IOSMDOptions = {
darkMode: colorScheme == 'dark',
backend: 'canvas',
drawComposer: false,
drawCredits: false,
drawLyrics: false,
@@ -43,15 +37,15 @@ const PartitionView = (props: PartitionViewProps) => {
autoResize: false,
};
// Turns note.Length or timestamp in ms
const timestampToMs = (timestamp: Fraction) => {
const timestampToMs = (timestamp: Fraction, wholeNoteLength: number) => {
return timestamp.RealValue * wholeNoteLength;
};
const getActualNoteLength = (note: Note) => {
let duration = timestampToMs(note.Length);
const getActualNoteLength = (note: Note, wholeNoteLength: number) => {
let duration = timestampToMs(note.Length, wholeNoteLength);
if (note.NoteTie) {
const firstNote = note.NoteTie.Notes.at(1);
if (Object.is(note.NoteTie.StartNote, note) && firstNote) {
duration += timestampToMs(firstNote.Length);
duration += timestampToMs(firstNote.Length, wholeNoteLength);
} else {
duration = 0;
}
@@ -59,99 +53,79 @@ const PartitionView = (props: PartitionViewProps) => {
return duration;
};
const playNotesUnderCursor = () => {
osmd!.cursor
.NotesUnderCursor()
.filter((note) => note.isRest() == false)
.filter((note) => note.Pitch) // Pitch Can be null, avoiding them
.forEach((note) => {
// Put your hands together for https://github.com/jimutt/osmd-audio-player/blob/master/src/internals/noteHelpers.ts
const fixedKey =
note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments.at(0)?.fixedKey ?? 0;
const midiNumber = note.halfTone - fixedKey * 12;
// console.log('Expecting midi ' + midiNumber);
const duration = getActualNoteLength(note);
const gain = note.ParentVoiceEntry.ParentVoice.Volume;
soundPlayer!.play(midiNumber.toString(), audioContext.currentTime, {
duration,
gain,
});
});
};
const getShortedNoteUnderCursor = () => {
return osmd!.cursor
.NotesUnderCursor()
.sort((n1, n2) => n1.Length.CompareTo(n2.Length))
.at(0);
};
useEffect(() => {
const _osmd = new OSMD(OSMD_DIV_ID, options);
Promise.all([
SoundFont.instrument(audioContext as unknown as AudioContext, 'electric_piano_1'),
_osmd.load(props.file),
]).then(([player]) => {
setSoundPlayer(player);
Promise.all([_osmd.load(props.file)]).then(() => {
_osmd.render();
_osmd.cursor.hide();
// Ty https://github.com/jimutt/osmd-audio-player/blob/ec205a6e46ee50002c1fa8f5999389447bba7bbf/src/PlaybackEngine.ts#LL77C12-L77C63
_osmd.cursor.show();
const bpm = _osmd.Sheet.HasBPMInfo ? _osmd.Sheet.getExpressionsStartTempoInBPM() : 60;
setWholeNoteLength(Math.round((60 / bpm) * 4000));
props.onPartitionReady();
const wholeNoteLength = Math.round((60 / bpm) * 4000);
const curPos = [];
while (!_osmd.cursor.iterator.EndReached) {
const notesToPlay = _osmd.cursor
.NotesUnderCursor()
.filter((note) => {
return note.isRest() == false && note.Pitch;
})
.map((note) => {
return {
note: note,
duration: getActualNoteLength(note, wholeNoteLength),
};
});
const shortestNotes = _osmd!.cursor
.NotesUnderCursor()
.sort((n1, n2) => n1.Length.CompareTo(n2.Length))
.at(0);
const ts = timestampToMs(
shortestNotes?.getAbsoluteTimestamp() ?? new Fraction(-1),
wholeNoteLength
);
const sNL = timestampToMs(
shortestNotes?.Length ?? new Fraction(-1),
wholeNoteLength
);
curPos.push({
offset: _osmd.cursor.cursorElement.offsetLeft,
notes: notesToPlay,
shortedNotes: shortestNotes,
sNinfos: {
ts,
sNL,
isRest: shortestNotes?.isRest(),
},
});
_osmd.cursor.next();
}
// console.log('curPos', curPos);
_osmd.cursor.reset();
_osmd.cursor.hide();
// console.log('timestamp cursor', _osmd.cursor.iterator.CurrentSourceTimestamp);
// console.log('timestamp cursor', _osmd.cursor.iterator.CurrentVoiceEntries);
// console.log('current measure index', _osmd.cursor.iterator.CurrentMeasureIndex);
const osmdCanvas = document.querySelector<HTMLCanvasElement>(
'#' + OSMD_DIV_ID + ' canvas'
);
// this should never happen this is done to silent ts linter about maybe null
if (!osmdCanvas) {
throw new Error('No canvas found');
}
// Ty https://github.com/jimutt/osmd-audio-player/blob/ec205a6e46ee50002c1fa8f5999389447bba7bbf/src/PlaybackEngine.ts#LL77C12-L77C63
props.onPartitionReady(
osmdCanvas.toDataURL(),
curPos.map((pos) => {
return {
x: pos.offset,
timing: pos.sNinfos.sNL,
timestamp: pos.sNinfos.ts,
notes: pos.notes,
};
})
);
// Do not show cursor before actuall start
});
setOsmd(_osmd);
}, []);
// Re-render manually (otherwise done by 'autoResize' option), to fix disappearing cursor
useEffect(() => {
if (osmd && osmd.IsReadyToRender()) {
osmd.render();
if (!osmd.cursor.hidden) {
osmd.cursor.show();
}
}
}, [dimensions]);
useEffect(() => {
if (!osmd || !soundPlayer) {
return;
}
if (props.timestamp > 0 && osmd.cursor.hidden && !osmd.cursor.iterator.EndReached) {
osmd.cursor.show();
playNotesUnderCursor();
return;
}
let previousCursorPosition = -1;
let currentCursorPosition = osmd.cursor.cursorElement.offsetLeft;
let shortestNote = getShortedNoteUnderCursor();
while (
!osmd.cursor.iterator.EndReached &&
(shortestNote?.isRest
? timestampToMs(shortestNote?.getAbsoluteTimestamp() ?? new Fraction(-1)) +
timestampToMs(shortestNote?.Length ?? new Fraction(-1)) <
props.timestamp
: timestampToMs(shortestNote?.getAbsoluteTimestamp() ?? new Fraction(-1)) <
props.timestamp)
) {
previousCursorPosition = currentCursorPosition;
osmd.cursor.next();
if (osmd.cursor.iterator.EndReached) {
osmd.cursor.hide(); // Lousy fix for https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/1338
soundPlayer.stop();
props.onEndReached();
} else {
// Shamelessly stolen from https://github.com/jimutt/osmd-audio-player/blob/ec205a6e46ee50002c1fa8f5999389447bba7bbf/src/PlaybackEngine.ts#LL223C7-L224C1
playNotesUnderCursor();
currentCursorPosition = osmd.cursor.cursorElement.offsetLeft;
document
.getElementById(OSMD_DIV_ID)
?.scrollBy(currentCursorPosition - previousCursorPosition, 0);
shortestNote = getShortedNoteUnderCursor();
}
}
}, [props.timestamp]);
return <div id={OSMD_DIV_ID} style={{ width: '100%', overflow: 'hidden' }} />;
};

View File

@@ -0,0 +1,192 @@
// create a simple phaser effect with a canvas that can be easily imported as a react component
import * as React from 'react';
import { useEffect } from 'react';
import Phaser from 'phaser';
import useColorScheme from '../../hooks/colorScheme';
import { RootState, useSelector } from '../../state/Store';
import { setSoundPlayer as setSPStore } from '../../state/SoundPlayerSlice';
import { useDispatch } from 'react-redux';
import { SplendidGrandPiano, CacheStorage } from 'smplr';
import { Note } from 'opensheetmusicdisplay';
let globalTimestamp = 0;
const globalStatus: 'playing' | 'paused' | 'stopped' = 'playing';
const isValidSoundPlayer = (soundPlayer: SplendidGrandPiano | undefined) => {
return soundPlayer && soundPlayer.loaded;
};
const myFindLast = <T,>(a: T[], p: (_: T, _2: number) => boolean) => {
for (let i = a.length - 1; i >= 0; i--) {
if (p(a[i]!, i)) {
return a[i];
}
}
return undefined;
};
const playNotes = (notes: PianoCursorNote[], soundPlayer: SplendidGrandPiano) => {
notes.forEach(({ note, duration }) => {
const fixedKey =
note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments.at(0)?.fixedKey ?? 0;
const midiNumber = note.halfTone - fixedKey * 12;
const gain = note.ParentVoiceEntry.ParentVoice.Volume;
soundPlayer.start({ note: midiNumber, duration, velocity: gain * 127 });
});
};
const getPianoScene = (
partitionB64: string,
cursorPositions: PianoCursorPosition[],
onEndReached: () => void,
soundPlayer: SplendidGrandPiano,
colorScheme: 'light' | 'dark'
) => {
class PianoScene extends Phaser.Scene {
async preload() {}
private cursorPositionsIdx = -1;
private partition!: Phaser.GameObjects.Image;
private cursor!: Phaser.GameObjects.Rectangle;
create() {
this.textures.addBase64('partition', partitionB64);
this.cursorPositionsIdx = -1;
this.cameras.main.setBackgroundColor(colorScheme === 'light' ? '#FFFFFF' : '#000000');
this.textures.on('onload', () => {
this.partition = this.add.image(0, 0, 'partition').setOrigin(0, 0);
this.cameras.main.setBounds(0, 0, this.partition.width, this.partition.height);
this.cursor = this.add.rectangle(0, 0, 30, 350, 0x31ef8c, 0.5).setOrigin(0, 0);
this.cameras.main.startFollow(this.cursor, true, 0.05, 0.05);
});
}
override update() {
const currentTimestamp = globalTimestamp;
const status = globalStatus;
if (status === 'playing') {
const transitionTime = 75;
const cP = myFindLast(cursorPositions, (cP: { timestamp: number }, idx: number) => {
if (
cP.timestamp < currentTimestamp + transitionTime &&
idx > this.cursorPositionsIdx
) {
this.cursorPositionsIdx = idx;
return true;
}
return false;
});
if (cP) {
playNotes(cP.notes, soundPlayer);
const tw = {
targets: this!.cursor,
x: cP!.x,
duration: transitionTime,
ease: 'Sine.easeInOut',
onComplete: undefined as (() => void) | undefined,
};
if (this.cursorPositionsIdx === cursorPositions.length - 1) {
tw.onComplete = () => {
onEndReached();
};
}
this.tweens.add(tw);
}
}
}
}
return PianoScene;
};
type PianoCursorNote = {
note: Note;
duration: number;
};
export type PianoCursorPosition = {
// offset in pixels
x: number;
// timestamp in ms
timing: number;
timestamp: number;
notes: PianoCursorNote[];
};
export type UpdateInfo = {
currentTimestamp: number;
status: 'playing' | 'paused' | 'stopped';
};
export type PhaserCanvasProps = {
partitionB64: string;
cursorPositions: PianoCursorPosition[];
onEndReached: () => void;
onPause: () => void;
onResume: () => void;
// Timestamp of the play session, in milisecond
timestamp: number;
};
const PhaserCanvas = ({
partitionB64,
cursorPositions,
onEndReached,
timestamp,
}: PhaserCanvasProps) => {
const colorScheme = useColorScheme();
const dispatch = useDispatch();
const soundPlayer = useSelector((state: RootState) => state.soundPlayer.soundPlayer);
const [game, setGame] = React.useState<Phaser.Game | null>(null);
globalTimestamp = timestamp;
useEffect(() => {
if (isValidSoundPlayer(soundPlayer)) {
return;
}
new SplendidGrandPiano(new AudioContext(), {
storage: new CacheStorage(),
})
.loaded()
.then((sp) => {
dispatch(setSPStore(sp));
});
}, []);
useEffect(() => {
if (!isValidSoundPlayer(soundPlayer) || !soundPlayer) return;
const pianoScene = getPianoScene(
partitionB64,
cursorPositions,
onEndReached,
soundPlayer,
colorScheme
);
const config = {
type: Phaser.AUTO,
parent: 'phaser-canvas',
width: 1000,
height: 400,
scene: pianoScene,
scale: {
mode: Phaser.Scale.FIT,
autoCenter: Phaser.Scale.CENTER_HORIZONTALLY,
},
};
setGame(new Phaser.Game(config));
return () => {
if (game) {
// currently the condition is always false
game.destroy(true);
}
};
}, [soundPlayer]);
return <div id="phaser-canvas"></div>;
};
export default PhaserCanvas;

View File

@@ -182,6 +182,7 @@ export const en = {
noRecentSearches: 'No recent searches',
avatar: 'Avatar',
changeIt: 'Change It',
verified: "Verified",
};
export const fr: typeof en = {
@@ -366,6 +367,7 @@ export const fr: typeof en = {
noRecentSearches: 'Aucune recherche récente',
avatar: 'Avatar',
changeIt: 'Modifier',
verified: "Verifié",
};
export const sp: typeof en = {
@@ -555,4 +557,5 @@ export const sp: typeof en = {
avatar: 'Avatar',
changeIt: 'Cambialo',
verified: "Verified"
};

View File

@@ -2,7 +2,7 @@ import Model, { ModelValidator } from './Model';
import * as yup from 'yup';
import ResponseHandler from './ResponseHandler';
export const SearchType = ['song', 'artist', 'album'] as const;
export const SearchType = ['song', 'artist', 'album', 'genre'] as const;
export type SearchType = (typeof SearchType)[number];
const SearchHistoryValidator = yup
@@ -27,7 +27,7 @@ export const SearchHistoryHandler: ResponseHandler<
interface SearchHistory extends Model {
query: string;
type: 'song' | 'artist' | 'album' | 'genre';
type: SearchType;
userId: number;
timestamp: Date;
}

View File

@@ -8,6 +8,7 @@ export const UserValidator = yup
username: yup.string().required(),
password: yup.string().required().nullable(),
email: yup.string().required(),
emailVerified: yup.boolean().required(),
googleID: yup.string().required().nullable(),
isGuest: yup.boolean().required(),
partyPlayed: yup.number().required(),
@@ -32,6 +33,7 @@ export const UserHandler: ResponseHandler<yup.InferType<typeof UserValidator>, U
interface User extends Model {
name: string;
email: string;
emailVerified: boolean;
googleID: string | null;
isGuest: boolean;
premium: boolean;

View File

@@ -50,6 +50,7 @@
"moti": "^0.22.0",
"native-base": "^3.4.17",
"opensheetmusicdisplay": "^1.7.5",
"phaser": "^3.60.0",
"react": "18.1.0",
"react-dom": "18.1.0",
"react-i18next": "^11.18.3",
@@ -68,8 +69,7 @@
"react-timer-hook": "^3.0.5",
"react-use-precision-timer": "^3.3.1",
"redux-persist": "^6.0.0",
"soundfont-player": "^0.12.0",
"standardized-audio-context": "^25.3.51",
"smplr": "^0.6.1",
"type-fest": "^3.6.0",
"yup": "^1.2.0"
},

View File

@@ -0,0 +1,19 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { SplendidGrandPiano } from 'smplr';
export const soundPlayerSlice = createSlice({
name: 'soundPlayer',
initialState: {
soundPlayer: undefined as SplendidGrandPiano | undefined,
},
reducers: {
setSoundPlayer: (state, action: PayloadAction<SplendidGrandPiano>) => {
state.soundPlayer = action.payload;
},
unsetSoundPlayer: (state) => {
state.soundPlayer = undefined;
},
},
});
export const { setSoundPlayer, unsetSoundPlayer } = soundPlayerSlice.actions;
export default soundPlayerSlice.reducer;

View File

@@ -1,5 +1,6 @@
import userReducer from '../state/UserSlice';
import settingsReduder from './SettingsSlice';
import settingsReducer from './SettingsSlice';
import SoundPlayerSliceReducer from './SoundPlayerSlice';
import { StateFromReducersMapObject, configureStore } from '@reduxjs/toolkit';
import languageReducer from './LanguageSlice';
import {
@@ -28,7 +29,8 @@ const persistConfig = {
const reducers = {
user: userReducer,
language: languageReducer,
settings: settingsReduder,
settings: settingsReducer,
soundPlayer: SoundPlayerSliceReducer,
};
type State = StateFromReducersMapObject<typeof reducers>;

View File

@@ -14,7 +14,7 @@
/* Language and Environment */
"target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [
"es2019",
"es2022",
"DOM"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
"jsx": "react-native" /* Specify what JSX code is generated. */,

View File

@@ -1,4 +1,5 @@
/* eslint-disable no-mixed-spaces-and-tabs */
import { StackActions } from '@react-navigation/native';
import React, { useEffect, useRef, useState } from 'react';
import { SafeAreaView, Platform, Animated } from 'react-native';
import * as ScreenOrientation from 'expo-screen-orientation';
@@ -15,20 +16,18 @@ import {
HStack,
} from 'native-base';
import IconButton from '../components/IconButton';
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
import { Ionicons } from '@expo/vector-icons';
import { RouteProps, useNavigation } from '../Navigation';
import { transformQuery, useQuery } from '../Queries';
import API from '../API';
import LoadingComponent, { LoadingView } from '../components/Loading';
import Constants from 'expo-constants';
import VirtualPiano from '../components/VirtualPiano/VirtualPiano';
import { strToKey, keyToStr, Note } from '../models/Piano';
import { useSelector } from 'react-redux';
import { RootState } from '../state/Store';
import { translate } from '../i18n/i18n';
import { ColorSchemeType } from 'native-base/lib/typescript/components/types';
import { useStopwatch } from 'react-use-precision-timer';
import PartitionView from '../components/PartitionView';
import PartitionCoord from '../components/PartitionCoord';
import TextButton from '../components/TextButton';
import { MIDIAccess, MIDIMessageEvent, requestMIDIAccess } from '@motiz88/react-native-midi';
import * as Linking from 'expo-linking';
@@ -78,7 +77,6 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
const webSocket = useRef<WebSocket>();
const [paused, setPause] = useState<boolean>(true);
const stopwatch = useStopwatch();
const [isVirtualPianoVisible, setVirtualPianoVisible] = useState<boolean>(false);
const [time, setTime] = useState(0);
const [partitionRendered, setPartitionRendered] = useState(false); // Used to know when partitionview can render
const [score, setScore] = useState(0); // Between 0 and 100
@@ -117,6 +115,12 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
);
};
const onEnd = () => {
stopwatch.stop();
if (webSocket.current?.readyState != WebSocket.OPEN) {
console.warn('onEnd: Websocket not open');
navigation.dispatch(StackActions.replace('Home'));
return;
}
webSocket.current?.send(
JSON.stringify({
type: 'end',
@@ -126,6 +130,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
const onMIDISuccess = (access: MIDIAccess) => {
const inputs = access.inputs;
let endMsgReceived = false; // Used to know if to go to error screen when websocket closes
if (inputs.size < 2) {
toast.show({ description: 'No MIDI Keyboard found' });
@@ -144,11 +149,25 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
})
);
};
webSocket.current.onclose = () => {
console.log('Websocket closed', endMsgReceived);
if (!endMsgReceived) {
toast.show({ description: 'Connection lost with Scorometer' });
// the special case when the front send the end message succesfully
// but the websocket is closed before the end message is received
// is not handled
return;
}
};
webSocket.current.onmessage = (message) => {
try {
const data = JSON.parse(message.data);
if (data.type == 'end') {
navigation.navigate('Score', { songId: song.data!.id, ...data });
endMsgReceived = true;
webSocket.current?.close();
navigation.dispatch(
StackActions.replace('Score', { songId: song.data!.id, ...data })
);
return;
}
const points = data.info.score;
@@ -222,7 +241,7 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
return () => {
ScreenOrientation.unlockAsync().catch(() => {});
onEnd();
stopwatch.stop();
clearInterval(interval);
};
}, []);
@@ -268,50 +287,17 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
</Animated.View>
</HStack>
<View style={{ flexGrow: 1, justifyContent: 'center' }}>
<PartitionView
<PartitionCoord
file={musixml.data}
timestamp={time}
onEndReached={onEnd}
onPause={onPause}
onResume={onResume}
onPartitionReady={() => setPartitionRendered(true)}
timestamp={Math.max(0, time)}
onEndReached={() => {
onEnd();
}}
/>
{!partitionRendered && <LoadingComponent />}
</View>
{isVirtualPianoVisible && (
<Column
style={{
display: 'flex',
justifyContent: 'flex-end',
alignItems: 'center',
height: '20%',
width: '100%',
}}
>
<VirtualPiano
onNoteDown={(note) => {
console.log('On note down', keyToStr(note));
}}
onNoteUp={(note) => {
console.log('On note up', keyToStr(note));
}}
showOctaveNumbers={true}
startNote={Note.C}
endNote={Note.B}
startOctave={2}
endOctave={5}
style={{
width: '80%',
height: '100%',
}}
highlightedNotes={[
{ key: strToKey('D3') },
{ key: strToKey('A#'), bgColor: '#00FF00' },
]}
/>
</Column>
)}
<Box
shadow={4}
style={{
@@ -355,20 +341,6 @@ const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
}
}}
/>
<IconButton
size="sm"
colorScheme="coolGray"
variant="solid"
icon={
<Icon
as={MaterialCommunityIcons}
name={isVirtualPianoVisible ? 'piano-off' : 'piano'}
/>
}
onPress={() => {
setVirtualPianoVisible(!isVirtualPianoVisible);
}}
/>
<Text>
{time < 0
? paused

View File

@@ -0,0 +1,35 @@
import { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import API from '../API';
import { Text } from 'native-base';
import { useNavigation } from '../Navigation';
import { useRoute } from '@react-navigation/native';
const VerifiedView = () => {
const navigation = useNavigation();
const route = useRoute();
const [failed, setFailed] = useState(false);
useEffect(() => {
async function run() {
try {
await API.fetch({
route: `/auth/verify?token=${(route.params as any).token}`,
method: 'PUT',
});
navigation.navigate('Home');
} catch {
setFailed(true);
}
}
run();
}, []);
return failed ? (
<Text>Email verification failed. The token has expired or is invalid.</Text>
) : (
<Text>Loading please wait</Text>
);
};
export default VerifiedView;

View File

@@ -55,6 +55,16 @@ const ProfileSettings = ({ navigation }: { navigation: any }) => {
},
},
},
{
type: 'text',
title: translate('verified'),
data: {
text: user.emailVerified ? 'verified' : 'not verified',
onPress: user.emailVerified
? undefined
: () => API.fetch({ route: '/auth/reverify', method: 'PUT' }),
},
},
{
type: 'text',
title: translate('avatar'),

View File

@@ -1239,13 +1239,6 @@
dependencies:
regenerator-runtime "^0.13.11"
"@babel/runtime@^7.22.3":
version "7.22.3"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.3.tgz#0a7fce51d43adbf0f7b517a71f4c3aaca92ebcbb"
integrity sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==
dependencies:
regenerator-runtime "^0.13.11"
"@babel/runtime@~7.5.4":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
@@ -5556,11 +5549,6 @@ address@^1.0.1:
resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e"
integrity sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==
adsr@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/adsr/-/adsr-1.0.1.tgz#a7bc08e5ef8a71e6364abc96fce7df1c44881cc3"
integrity sha512-thr9LK4jxApOzBA33IWOA83bXJFbyfbeozpHXyrMQOIhUni198uRxXqDhobW0S/51iokqty2Yz2WbLZbE6tntQ==
agent-base@6, agent-base@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -6014,19 +6002,6 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
audio-loader@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/audio-loader/-/audio-loader-0.5.0.tgz#9c125d1b25c33cd9626084054d9f6b7f31ddc908"
integrity sha512-mEoYRjZhqkBSen/X9i2PNosqvafEsur8bI5MNoPr0wsJu9Nzlul3Yv1elYeMPsXxTxYhXLY8AZlScBvaK4mydg==
automation-events@^6.0.4:
version "6.0.4"
resolved "https://registry.yarnpkg.com/automation-events/-/automation-events-6.0.4.tgz#a308501319b9f921de7165e0b1a201b46cd1ab59"
integrity sha512-3C/7GtIB1rEwXfSEMUaJRZJFaDJWyiZ3g+Z1HWVAZj+SYZDGKZiZKTZ+Kfq0Lmnb0hL5RXtJ5prfMXbC10evzA==
dependencies:
"@babel/runtime" "^7.22.3"
tslib "^2.5.3"
autoprefixer@^9.8.6:
version "9.8.8"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.8.tgz#fd4bd4595385fa6f06599de749a4d5f7a474957a"
@@ -8921,6 +8896,11 @@ eventemitter3@^4.0.0:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
eventemitter3@^5.0.0:
version "5.0.1"
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4"
integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==
events@^3.0.0, events@^3.2.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
@@ -13409,11 +13389,6 @@ midi-player-js@^2.0.16:
resolved "https://registry.yarnpkg.com/midi-player-js/-/midi-player-js-2.0.16.tgz#41167859e3f430e55eeb962887cb498726d6c570"
integrity sha512-Y1yCRvvSjJjT5J4U8T4XTCDF1FLXtw8Otvq5BAmIob/2cj10aQUDrPDFByTWeuMRPu6/nLhusROc1DuTLCzRnw==
midimessage@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/midimessage/-/midimessage-1.0.5.tgz#ad99f04d863a053a2563d553c5bf35070b48802c"
integrity sha512-MPJ2tDupFOfZB5/PLp8fri1IS4fd9hPj0Bio//FBhWRQ+TsJA7/49CF1aJyraDxa0Jq8zMHAwrwXl2GINvLvgw==
miller-rabin@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -14016,16 +13991,6 @@ normalize-url@^6.0.1:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
note-parser@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/note-parser/-/note-parser-1.1.0.tgz#12e9f17e51450ec994f1364a01982c22667b8e6b"
integrity sha512-YTqWQBsRp40EFrEznnkGtmx68gcgOQ8CdoBspqGBA3G1/4mJwIYbDe/vuNpX3oGX2DhP7b1dBgTmj7p3Zr0P1Q==
note-parser@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/note-parser/-/note-parser-2.0.1.tgz#2438fd57a46894b402b3a2071798660129c8fbc1"
integrity sha512-w9o6Fv46y3NsFxeezTZSmftBtUM/ypme6iZWVrTJvvsD5RN+w0XNDePWtfreNrZFL3jSjBFhadPoXb+pJO4UdA==
npm-package-arg@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-7.0.0.tgz#52cdf08b491c0c59df687c4c925a89102ef794a5"
@@ -14749,6 +14714,13 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
phaser@^3.60.0:
version "3.60.0"
resolved "https://registry.yarnpkg.com/phaser/-/phaser-3.60.0.tgz#8a555623e64c707482e6321485b4bda84604590d"
integrity sha512-IKUy35EnoEVcl2EmJ8WOyK4X8OoxHYdlhZLgRGpNrvD1fEagYffhVmwHcapE/tGiLgyrnezmXIo5RrH2NcrTHw==
dependencies:
eventemitter3 "^5.0.0"
picocolors@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f"
@@ -16599,15 +16571,6 @@ safe-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
sample-player@^0.5.5:
version "0.5.5"
resolved "https://registry.yarnpkg.com/sample-player/-/sample-player-0.5.5.tgz#bc35bea3449c6fa972528f022a9bbc2872195637"
integrity sha512-VQ9pXPJ1m/eTH8QK6OQ8Dn/HSVToNyY9w9vnv+y/yjkJeRm87tJ/gBEm66jItfSLhKe6VG1DfX8+oT+Mg7QUpg==
dependencies:
adsr "^1.0.0"
midimessage "^1.0.5"
note-parser "^1.1.0"
sane@^4.0.3:
version "4.1.0"
resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded"
@@ -16975,6 +16938,11 @@ smart-buffer@^4.2.0:
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
smplr@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/smplr/-/smplr-0.6.1.tgz#f24cbe7ce3ad318bb6ce226d9aa933d1cab7dc56"
integrity sha512-040QDtYRavqIje9346zWBYDc3oN/ARSZmheOGELAujQVYr3p4e8nrOsojH3VQsE0zcrAhjJ4MDeg74qIHQCC7A==
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@@ -17073,15 +17041,6 @@ socks@^2.6.2:
ip "^2.0.0"
smart-buffer "^4.2.0"
soundfont-player@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/soundfont-player/-/soundfont-player-0.12.0.tgz#2b26149f28aba471d2285d3df9a2e1e5793ceaf1"
integrity sha512-8BJIsAt7h1PK3thSZDgF6zecgGhYkK74JnZO8WRZi3h34qG6H/DYlnv7cpRvL7Q9C8N6qld4Qwj7nJsX1gYjEA==
dependencies:
audio-loader "^0.5.0"
note-parser "^2.0.0"
sample-player "^0.5.5"
source-list-map@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
@@ -17254,15 +17213,6 @@ stacktrace-parser@^0.1.3:
dependencies:
type-fest "^0.7.1"
standardized-audio-context@^25.3.51:
version "25.3.51"
resolved "https://registry.yarnpkg.com/standardized-audio-context/-/standardized-audio-context-25.3.51.tgz#0eb54629355d1ddf2070897e586eaa8dfec8c0f5"
integrity sha512-+YPccvetw8wqWo0pv6lo5aDeUq+2WHL/S+8AWdrLKG1jMlhJZqK/GjNF/88q6jXAHal32Msc1xPx3uGrx8RPdQ==
dependencies:
"@babel/runtime" "^7.22.3"
automation-events "^6.0.4"
tslib "^2.5.3"
state-toggle@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/state-toggle/-/state-toggle-1.0.3.tgz#e123b16a88e143139b09c6852221bc9815917dfe"
@@ -18108,11 +18058,6 @@ tslib@^2, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.1,
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e"
integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==
tslib@^2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.3.tgz#24944ba2d990940e6e982c4bea147aba80209913"
integrity sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"

View File

@@ -1,4 +1,4 @@
FROM python:latest
FROM python:3.10
RUN wget -q -O /tmp/websocketd.zip \
https://github.com/joewalnes/websocketd/releases/download/v0.4.1/websocketd-0.4.1-linux_amd64.zip \
&& unzip /tmp/websocketd.zip -d /tmp/websocketd && mv /tmp/websocketd/websocketd /usr/bin \

View File

@@ -1,4 +1,4 @@
FROM python:latest
FROM python:3.10
RUN wget -q -O /tmp/websocketd.zip \
https://github.com/joewalnes/websocketd/releases/download/v0.4.1/websocketd-0.4.1-linux_amd64.zip \
&& unzip /tmp/websocketd.zip -d /tmp/websocketd && mv /tmp/websocketd/websocketd /usr/bin \

21
shell.nix Normal file
View File

@@ -0,0 +1,21 @@
{pkgs ? import <nixpkgs> {}}:
pkgs.mkShell {
nativeBuildInputs = [pkgs.bashInteractive];
buildInputs = with pkgs; [
nodePackages.prisma
nodePackages."@nestjs/cli"
nodePackages.npm
nodejs_16
yarn
python3
pkg-config
];
shellHook = with pkgs; ''
# export PRISMA_MIGRATION_ENGINE_BINARY="${prisma-engines}/bin/migration-engine"
# export PRISMA_QUERY_ENGINE_BINARY="${prisma-engines}/bin/query-engine"
export PRISMA_QUERY_ENGINE_LIBRARY="${prisma-engines}/lib/libquery_engine.node"
export PRISMA_INTROSPECTION_ENGINE_BINARY="${prisma-engines}/bin/introspection-engine"
export PRISMA_FMT_BINARY="${prisma-engines}/bin/prisma-fmt"
export DATABASE_URL=postgresql://user:eip@localhost:5432/chromacase
'';
}