Merge branch 'main' into feature/adc/#243-leaderboard
This commit is contained in:
5
.github/workflows/CI.yml
vendored
5
.github/workflows/CI.yml
vendored
@@ -63,10 +63,10 @@ jobs:
|
||||
run: yarn install
|
||||
|
||||
- name: 🏗 Setup Expo
|
||||
uses: expo/expo-github-action@v7
|
||||
uses: expo/expo-github-action@v8
|
||||
with:
|
||||
expo-version: latest
|
||||
eas-version: 3.3.1
|
||||
eas-version: latest
|
||||
token: ${{ secrets.EXPO_TOKEN }}
|
||||
- name: Build Web App
|
||||
uses: docker/build-push-action@v3
|
||||
@@ -157,7 +157,6 @@ jobs:
|
||||
Deployement_Docker:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.ref == 'refs/heads/main'
|
||||
needs: [ Test_Back ]
|
||||
environment: Production
|
||||
|
||||
steps:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
FROM node:17
|
||||
WORKDIR /app
|
||||
CMD npx prisma generate ; npx prisma migrate dev ; npm run start:dev
|
||||
CMD npm i ; npx prisma generate ; npx prisma migrate dev ; npm run start:dev
|
||||
|
||||
1627
back/package-lock.json
generated
1627
back/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,12 +35,16 @@
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/passport": "^1.0.12",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"canvas": "^2.11.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.13.2",
|
||||
"json-logger-service": "^9.0.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"cross-blob": "^3.0.2",
|
||||
"fs": "^0.0.1-security",
|
||||
"jsdom": "^22.1.0",
|
||||
"json-logger-service": "^9.0.1",
|
||||
"node-fetch": "^2.6.12",
|
||||
"nodemailer": "^6.9.5",
|
||||
"opensheetmusicdisplay": "^1.8.4",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
@@ -81,7 +85,8 @@
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
"ts",
|
||||
"mjs"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
|
||||
794
back/src/assetsgenerator/generateImages_browserless.js
Normal file
794
back/src/assetsgenerator/generateImages_browserless.js
Normal file
@@ -0,0 +1,794 @@
|
||||
// import Blob from "cross-blob";
|
||||
import FS from "fs";
|
||||
import jsdom from "jsdom";
|
||||
//import headless_gl from "gl"; // this is now imported dynamically in a try catch, in case gl install fails, see #1160
|
||||
import * as OSMD from "opensheetmusicdisplay"; // window needs to be available before we can require OSMD
|
||||
|
||||
let Blob;
|
||||
|
||||
/*
|
||||
Render each OSMD sample, grab the generated images, andg
|
||||
dump them into a local directory as PNG or SVG files.
|
||||
|
||||
inspired by Vexflow's generate_png_images and vexflow-tests.js
|
||||
|
||||
This can be used to generate PNGs or SVGs from OSMD without a browser.
|
||||
It's also used with the visual regression test system (using PNGs) in
|
||||
`tools/visual_regression.sh`
|
||||
(see package.json, used with npm run generate:blessed and generate:current, then test:visual).
|
||||
|
||||
Note: this script needs to "fake" quite a few browser elements, like window, document,
|
||||
and a Canvas HTMLElement (for PNG) or the DOM (for SVG) ,
|
||||
which otherwise are missing in pure nodejs, causing errors in OSMD.
|
||||
For PNG it needs the canvas package installed.
|
||||
There are also some hacks needed to set the container size (offsetWidth) correctly.
|
||||
|
||||
Otherwise you'd need to run a headless browser, which is way slower,
|
||||
see the semi-obsolete generateDiffImagesPuppeteerLocalhost.js
|
||||
*/
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
const timestampToMs = (timestamp, wholeNoteLength) => {
|
||||
return timestamp.RealValue * wholeNoteLength;
|
||||
};
|
||||
const getActualNoteLength = (note, wholeNoteLength) => {
|
||||
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, wholeNoteLength);
|
||||
} else {
|
||||
duration = 0;
|
||||
}
|
||||
}
|
||||
return duration;
|
||||
};
|
||||
|
||||
function getCursorPositions(osmd, filename, partitionDims) {
|
||||
osmd.cursor.show();
|
||||
const bpm = osmd.Sheet.HasBPMInfo
|
||||
? osmd.Sheet.getExpressionsStartTempoInBPM()
|
||||
: 60;
|
||||
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) => {
|
||||
const fixedKey =
|
||||
note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments.at(0)
|
||||
?.fixedKey ?? 0;
|
||||
const midiNumber = note.halfTone - fixedKey * 12;
|
||||
const gain = note.ParentVoiceEntry.ParentVoice.Volume;
|
||||
return {
|
||||
note: midiNumber,
|
||||
gain: gain,
|
||||
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 OSMD.Fraction(-1),
|
||||
wholeNoteLength,
|
||||
);
|
||||
const sNL = timestampToMs(
|
||||
shortestNotes?.Length ?? new OSMD.Fraction(-1),
|
||||
wholeNoteLength,
|
||||
);
|
||||
curPos.push({
|
||||
x: parseFloat(osmd.cursor.cursorElement.style.left),
|
||||
y: parseFloat(osmd.cursor.cursorElement.style.top),
|
||||
width: osmd.cursor.cursorElement.width,
|
||||
height: osmd.cursor.cursorElement.height,
|
||||
notes: notesToPlay,
|
||||
timestamp: ts,
|
||||
timing: sNL,
|
||||
});
|
||||
osmd.cursor.next();
|
||||
}
|
||||
osmd.cursor.reset();
|
||||
osmd.cursor.hide();
|
||||
|
||||
const cursorsFilename = `${imageDir}/${filename}.json`;
|
||||
FS.writeFileSync(
|
||||
cursorsFilename,
|
||||
JSON.stringify({
|
||||
pageWidth: partitionDims[0],
|
||||
pageHeight: partitionDims[1],
|
||||
cursors: curPos,
|
||||
}),
|
||||
);
|
||||
console.log(`Saved cursor positions to ${cursorsFilename}`);
|
||||
}
|
||||
|
||||
// global variables
|
||||
// (without these being global, we'd have to pass many of these values to the generateSampleImage function)
|
||||
// eslint-disable-next-line prefer-const
|
||||
let assetName;
|
||||
let sampleDir;
|
||||
let imageDir;
|
||||
let imageFormat;
|
||||
let pageWidth;
|
||||
let pageHeight;
|
||||
let filterRegex;
|
||||
let mode;
|
||||
let debugSleepTimeString;
|
||||
let skyBottomLinePreference;
|
||||
let pageFormat;
|
||||
|
||||
export async function generateSongAssets(
|
||||
assetName_,
|
||||
sampleDir_,
|
||||
imageDir_,
|
||||
imageFormat_,
|
||||
pageWidth_,
|
||||
pageHeight_,
|
||||
filterRegex_,
|
||||
mode_,
|
||||
debugSleepTimeString_,
|
||||
skyBottomLinePreference_,
|
||||
) {
|
||||
assetName = assetName_;
|
||||
sampleDir = sampleDir_;
|
||||
imageDir = imageDir_;
|
||||
imageFormat = imageFormat_;
|
||||
pageWidth = pageWidth_;
|
||||
pageHeight = pageHeight_;
|
||||
filterRegex = filterRegex_;
|
||||
mode = mode_;
|
||||
debugSleepTimeString = debugSleepTimeString_;
|
||||
skyBottomLinePreference = skyBottomLinePreference_;
|
||||
imageFormat = imageFormat?.toLowerCase();
|
||||
eval(`import("cross-blob")`).then((module) => {
|
||||
Blob = module.default;
|
||||
});
|
||||
debug("" + sampleDir + " " + imageDir + " " + imageFormat);
|
||||
|
||||
if (!mode) {
|
||||
mode = "";
|
||||
}
|
||||
if (
|
||||
!assetName ||
|
||||
!sampleDir ||
|
||||
!imageDir ||
|
||||
(imageFormat !== "png" && imageFormat !== "svg")
|
||||
) {
|
||||
console.log(
|
||||
"usage: " +
|
||||
// eslint-disable-next-line max-len
|
||||
"node test/Util/generateImages_browserless.mjs osmdBuildDir sampleDirectory imageDirectory svg|png [width|0] [height|0] [filterRegex|all|allSmall] [--debug|--osmdtesting] [debugSleepTime]",
|
||||
);
|
||||
console.log(
|
||||
" (use pageWidth and pageHeight 0 to not divide the rendering into pages (endless page))",
|
||||
);
|
||||
console.log(
|
||||
' (use "all" to skip filterRegex parameter. "allSmall" with --osmdtesting skips two huge OSMD samples that take forever to render)',
|
||||
);
|
||||
console.log(
|
||||
"example: node test/Util/generateImages_browserless.mjs ../../build ./test/data/ ./export png",
|
||||
);
|
||||
console.log(
|
||||
"Error: need osmdBuildDir, sampleDir, imageDir and svg|png arguments. Exiting.",
|
||||
);
|
||||
Promise.reject(
|
||||
"Error: need osmdBuildDir, sampleDir, imageDir and svg|png arguments. Exiting.",
|
||||
);
|
||||
}
|
||||
await init();
|
||||
}
|
||||
|
||||
// let OSMD; // can only be required once window was simulated
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
|
||||
async function init() {
|
||||
debug("init");
|
||||
|
||||
const osmdTestingMode = mode.includes("osmdtesting"); // can also be --debugosmdtesting
|
||||
const osmdTestingSingleMode = mode.includes("osmdtestingsingle");
|
||||
const DEBUG = mode.startsWith("--debug");
|
||||
// const debugSleepTime = Number.parseInt(process.env.GENERATE_DEBUG_SLEEP_TIME) || 0; // 5000 works for me [sschmidTU]
|
||||
if (DEBUG) {
|
||||
// debug(' (note that --debug slows down the script by about 0.3s per file, through logging)')
|
||||
const debugSleepTimeMs = Number.parseInt(debugSleepTimeString, 10);
|
||||
if (debugSleepTimeMs > 0) {
|
||||
debug("debug sleep time: " + debugSleepTimeString);
|
||||
await sleep(Number.parseInt(debugSleepTimeMs, 10));
|
||||
// [VSCode] apparently this is necessary for the debugger to attach itself in time before the program closes.
|
||||
// sometimes this is not enough, so you may have to try multiple times or increase the sleep timer. Unfortunately debugging nodejs isn't easy.
|
||||
}
|
||||
}
|
||||
debug("sampleDir: " + sampleDir, DEBUG);
|
||||
debug("imageDir: " + imageDir, DEBUG);
|
||||
debug("imageFormat: " + imageFormat, DEBUG);
|
||||
|
||||
pageFormat = "Endless";
|
||||
pageWidth = Number.parseInt(pageWidth, 10);
|
||||
pageHeight = Number.parseInt(pageHeight, 10);
|
||||
const endlessPage = !(pageHeight > 0 && pageWidth > 0);
|
||||
if (!endlessPage) {
|
||||
pageFormat = `${pageWidth}x${pageHeight}`;
|
||||
}
|
||||
|
||||
// ---- hacks to fake Browser elements OSMD and Vexflow need, like window, document, and a canvas HTMLElement ----
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const dom = new jsdom.JSDOM("<!DOCTYPE html></html>");
|
||||
// eslint-disable-next-line no-global-assign
|
||||
// window = dom.window;
|
||||
// eslint-disable-next-line no-global-assign
|
||||
// document = dom.window.document;
|
||||
|
||||
// eslint-disable-next-line no-global-assign
|
||||
global.window = dom.window;
|
||||
// eslint-disable-next-line no-global-assign
|
||||
global.document = window.document;
|
||||
//window.console = console; // probably does nothing
|
||||
global.HTMLElement = window.HTMLElement;
|
||||
global.HTMLAnchorElement = window.HTMLAnchorElement;
|
||||
global.XMLHttpRequest = window.XMLHttpRequest;
|
||||
global.DOMParser = window.DOMParser;
|
||||
global.Node = window.Node;
|
||||
if (imageFormat === "png") {
|
||||
global.Canvas = window.Canvas;
|
||||
}
|
||||
|
||||
// For WebGLSkyBottomLineCalculatorBackend: Try to import gl dynamically
|
||||
// this is so that the script doesn't fail if gl could not be installed,
|
||||
// which can happen in some linux setups where gcc-11 is installed, see #1160
|
||||
try {
|
||||
const { default: headless_gl } = await import("gl");
|
||||
const oldCreateElement = document.createElement.bind(document);
|
||||
document.createElement = function (tagName, options) {
|
||||
if (tagName.toLowerCase() === "canvas") {
|
||||
const canvas = oldCreateElement(tagName, options);
|
||||
const oldGetContext = canvas.getContext.bind(canvas);
|
||||
canvas.getContext = function (contextType, contextAttributes) {
|
||||
if (
|
||||
contextType.toLowerCase() === "webgl" ||
|
||||
contextType.toLowerCase() === "experimental-webgl"
|
||||
) {
|
||||
const gl = headless_gl(
|
||||
canvas.width,
|
||||
canvas.height,
|
||||
contextAttributes,
|
||||
);
|
||||
gl.canvas = canvas;
|
||||
return gl;
|
||||
} else {
|
||||
return oldGetContext(contextType, contextAttributes);
|
||||
}
|
||||
};
|
||||
return canvas;
|
||||
} else {
|
||||
return oldCreateElement(tagName, options);
|
||||
}
|
||||
};
|
||||
} catch {
|
||||
if (skyBottomLinePreference === "--webgl") {
|
||||
debug(
|
||||
"WebGL image generation was requested but gl is not installed; using non-WebGL generation.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// fix Blob not found (to support external modules like is-blob)
|
||||
global.Blob = Blob;
|
||||
|
||||
const div = document.createElement("div");
|
||||
div.id = "browserlessDiv";
|
||||
document.body.appendChild(div);
|
||||
// const canvas = document.createElement('canvas')
|
||||
// div.canvas = document.createElement('canvas')
|
||||
|
||||
const zoom = 1.0;
|
||||
// width of the div / PNG generated
|
||||
let width = pageWidth * zoom;
|
||||
// TODO sometimes the width is way too small for the score, may need to adjust zoom.
|
||||
if (endlessPage) {
|
||||
width = 1440;
|
||||
}
|
||||
let height = pageHeight;
|
||||
if (endlessPage) {
|
||||
height = 32767;
|
||||
}
|
||||
div.width = width;
|
||||
div.height = height;
|
||||
// div.offsetWidth = width; // doesn't work, offsetWidth is always 0 from this. see below
|
||||
// div.clientWidth = width;
|
||||
// div.clientHeight = height;
|
||||
// div.scrollHeight = height;
|
||||
// div.scrollWidth = width;
|
||||
div.setAttribute("width", width);
|
||||
div.setAttribute("height", height);
|
||||
div.setAttribute("offsetWidth", width);
|
||||
// debug('div.offsetWidth: ' + div.offsetWidth, DEBUG) // 0 here, set correctly later
|
||||
// debug('div.height: ' + div.height, DEBUG)
|
||||
|
||||
// hack: set offsetWidth reliably
|
||||
Object.defineProperties(window.HTMLElement.prototype, {
|
||||
offsetLeft: {
|
||||
get: function () {
|
||||
return parseFloat(window.getComputedStyle(this).marginTop) || 0;
|
||||
},
|
||||
},
|
||||
offsetTop: {
|
||||
get: function () {
|
||||
return parseFloat(window.getComputedStyle(this).marginTop) || 0;
|
||||
},
|
||||
},
|
||||
offsetHeight: {
|
||||
get: function () {
|
||||
return height;
|
||||
},
|
||||
},
|
||||
offsetWidth: {
|
||||
get: function () {
|
||||
return width;
|
||||
},
|
||||
},
|
||||
});
|
||||
debug("div.offsetWidth: " + div.offsetWidth, DEBUG);
|
||||
debug("div.height: " + div.height, DEBUG);
|
||||
// ---- end browser hacks (hopefully) ----
|
||||
|
||||
// load globally
|
||||
|
||||
// Create the image directory if it doesn't exist.
|
||||
FS.mkdirSync(imageDir, { recursive: true });
|
||||
|
||||
// const sampleDirFilenames = FS.readdirSync(sampleDir);
|
||||
let samplesToProcess = []; // samples we want to process/generate pngs of, excluding the filtered out files/filenames
|
||||
|
||||
// sampleDir is the direct path to a single file but is then only keept as a the directory containing the file
|
||||
if (sampleDir.match("^.*(.xml)|(.musicxml)|(.mxl)$")) {
|
||||
let pathParts = sampleDir.split("/");
|
||||
let filename = pathParts[pathParts.length - 1];
|
||||
sampleDir = pathParts.slice(0, pathParts.length - 1).join("/");
|
||||
samplesToProcess.push(filename);
|
||||
} else {
|
||||
debug("not a correct extension sampleDir: " + sampleDir, DEBUG);
|
||||
}
|
||||
// for (const sampleFilename of sampleDirFilenames) {
|
||||
// if (osmdTestingMode && filterRegex === "allSmall") {
|
||||
// if (sampleFilename.match("^(Actor)|(Gounod)")) {
|
||||
// // TODO maybe filter by file size instead
|
||||
// debug("filtering big file: " + sampleFilename, DEBUG);
|
||||
// continue;
|
||||
// }
|
||||
// }
|
||||
// // eslint-disable-next-line no-useless-escape
|
||||
// if (sampleFilename.match("^.*(.xml)|(.musicxml)|(.mxl)$")) {
|
||||
// // debug('found musicxml/mxl: ' + sampleFilename)
|
||||
// samplesToProcess.push(sampleFilename);
|
||||
// } else {
|
||||
// debug("discarded file/directory: " + sampleFilename, DEBUG);
|
||||
// }
|
||||
// }
|
||||
|
||||
// filter samples to process by regex if given
|
||||
if (
|
||||
filterRegex &&
|
||||
filterRegex !== "" &&
|
||||
filterRegex !== "all" &&
|
||||
!(osmdTestingMode && filterRegex === "allSmall")
|
||||
) {
|
||||
debug("filtering samples for regex: " + filterRegex, DEBUG);
|
||||
samplesToProcess = samplesToProcess.filter((filename) =>
|
||||
filename.match(filterRegex),
|
||||
);
|
||||
debug(`found ${samplesToProcess.length} matches: `, DEBUG);
|
||||
for (let i = 0; i < samplesToProcess.length; i++) {
|
||||
debug(samplesToProcess[i], DEBUG);
|
||||
}
|
||||
}
|
||||
|
||||
const backend = imageFormat === "png" ? "canvas" : "svg";
|
||||
const osmdInstance = new OSMD.OpenSheetMusicDisplay(div, {
|
||||
autoResize: false,
|
||||
backend: backend,
|
||||
pageBackgroundColor: "#FFFFFF",
|
||||
pageFormat: pageFormat,
|
||||
// defaultFontFamily: 'Arial',
|
||||
drawTitle: false,
|
||||
renderSingleHorizontalStaffline: true,
|
||||
drawComposer: false,
|
||||
drawCredits: false,
|
||||
drawLyrics: false,
|
||||
drawPartNames: false,
|
||||
followCursor: false,
|
||||
cursorsOptions: [{ type: 0, color: "green", alpha: 0.5, follow: false }],
|
||||
});
|
||||
// for more options check OSMDOptions.ts
|
||||
|
||||
// you can set finer-grained rendering/engraving settings in EngravingRules:
|
||||
// osmdInstance.EngravingRules.TitleTopDistance = 5.0 // 5.0 is default
|
||||
// (unless in osmdTestingMode, these will be reset with drawingParameters default)
|
||||
// osmdInstance.EngravingRules.PageTopMargin = 5.0 // 5 is default
|
||||
// osmdInstance.EngravingRules.PageBottomMargin = 5.0 // 5 is default. <5 can cut off scores that extend in the last staffline
|
||||
// note that for now the png and canvas will still have the height given in the script argument,
|
||||
// so even with a margin of 0 the image will be filled to the full height.
|
||||
// osmdInstance.EngravingRules.PageLeftMargin = 5.0 // 5 is default
|
||||
// osmdInstance.EngravingRules.PageRightMargin = 5.0 // 5 is default
|
||||
// osmdInstance.EngravingRules.MetronomeMarkXShift = -8; // -6 is default
|
||||
// osmdInstance.EngravingRules.DistanceBetweenVerticalSystemLines = 0.15; // 0.35 is default
|
||||
// for more options check EngravingRules.ts (though not all of these are meant and fully supported to be changed at will)
|
||||
|
||||
if (DEBUG) {
|
||||
osmdInstance.setLogLevel("debug");
|
||||
// debug(`osmd PageFormat: ${osmdInstance.EngravingRules.PageFormat.width}x${osmdInstance.EngravingRules.PageFormat.height}`)
|
||||
debug(
|
||||
`osmd PageFormat idString: ${osmdInstance.EngravingRules.PageFormat.idString}`,
|
||||
);
|
||||
debug("PageHeight: " + osmdInstance.EngravingRules.PageHeight);
|
||||
} else {
|
||||
osmdInstance.setLogLevel("info"); // doesn't seem to work, log.debug still logs
|
||||
}
|
||||
|
||||
debug(
|
||||
"[OSMD.generateImages] starting loop over samples, saving images to " +
|
||||
imageDir,
|
||||
DEBUG,
|
||||
);
|
||||
for (let i = 0; i < samplesToProcess.length; i++) {
|
||||
const sampleFilename = samplesToProcess[i];
|
||||
debug("sampleFilename: " + sampleFilename, DEBUG);
|
||||
|
||||
await generateSampleImage(
|
||||
sampleFilename,
|
||||
sampleDir,
|
||||
osmdInstance,
|
||||
osmdTestingMode,
|
||||
{},
|
||||
DEBUG,
|
||||
);
|
||||
|
||||
if (
|
||||
osmdTestingMode &&
|
||||
!osmdTestingSingleMode &&
|
||||
sampleFilename.startsWith("Beethoven") &&
|
||||
sampleFilename.includes("Geliebte")
|
||||
) {
|
||||
// generate one more testing image with skyline and bottomline. (startsWith 'Beethoven' don't catch the function test)
|
||||
await generateSampleImage(
|
||||
sampleFilename,
|
||||
sampleDir,
|
||||
osmdInstance,
|
||||
osmdTestingMode,
|
||||
{ skyBottomLine: true },
|
||||
DEBUG,
|
||||
);
|
||||
// generate one more testing image with GraphicalNote positions
|
||||
await generateSampleImage(
|
||||
sampleFilename,
|
||||
sampleDir,
|
||||
osmdInstance,
|
||||
osmdTestingMode,
|
||||
{ boundingBoxes: "VexFlowGraphicalNote" },
|
||||
DEBUG,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
debug("done, exiting.");
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
// let maxRss = 0, maxRssFilename = '' // to log memory usage (debug)
|
||||
async function generateSampleImage(
|
||||
sampleFilename,
|
||||
directory,
|
||||
osmdInstance,
|
||||
osmdTestingMode,
|
||||
options = {},
|
||||
DEBUG = false,
|
||||
) {
|
||||
function makeSkyBottomLineOptions() {
|
||||
const preference = skyBottomLinePreference ?? "";
|
||||
if (preference === "--batch") {
|
||||
return {
|
||||
preferredSkyBottomLineBatchCalculatorBackend: 0, // plain
|
||||
skyBottomLineBatchCriteria: 0, // use batch algorithm only
|
||||
};
|
||||
} else if (preference === "--webgl") {
|
||||
return {
|
||||
preferredSkyBottomLineBatchCalculatorBackend: 1, // webgl
|
||||
skyBottomLineBatchCriteria: 0, // use batch algorithm only
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
preferredSkyBottomLineBatchCalculatorBackend: 0, // plain
|
||||
skyBottomLineBatchCriteria: Infinity, // use non-batch algorithm only
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const samplePath = directory + "/" + sampleFilename;
|
||||
let loadParameter = FS.readFileSync(samplePath);
|
||||
|
||||
if (sampleFilename.endsWith(".mxl")) {
|
||||
loadParameter = await OSMD.MXLHelper.MXLtoXMLstring(loadParameter);
|
||||
} else {
|
||||
loadParameter = loadParameter.toString();
|
||||
}
|
||||
// debug('loadParameter: ' + loadParameter)
|
||||
// debug('typeof loadParameter: ' + typeof loadParameter)
|
||||
|
||||
// set sample-specific options for OSMD visual regression testing
|
||||
let includeSkyBottomLine = false;
|
||||
let drawBoundingBoxString;
|
||||
let isTestOctaveShiftInvisibleInstrument;
|
||||
let isTestInvisibleMeasureNotAffectingLayout;
|
||||
if (osmdTestingMode) {
|
||||
const isFunctionTestAutobeam = sampleFilename.startsWith(
|
||||
"OSMD_function_test_autobeam",
|
||||
);
|
||||
const isFunctionTestAutoColoring = sampleFilename.startsWith(
|
||||
"OSMD_function_test_auto-custom-coloring",
|
||||
);
|
||||
const isFunctionTestSystemAndPageBreaks = sampleFilename.startsWith(
|
||||
"OSMD_Function_Test_System_and_Page_Breaks",
|
||||
);
|
||||
const isFunctionTestDrawingRange = sampleFilename.startsWith(
|
||||
"OSMD_function_test_measuresToDraw_",
|
||||
);
|
||||
const defaultOrCompactTightMode = sampleFilename.startsWith(
|
||||
"OSMD_Function_Test_Container_height",
|
||||
)
|
||||
? "compacttight"
|
||||
: "default";
|
||||
const isTestFlatBeams = sampleFilename.startsWith("test_drum_tuplet_beams");
|
||||
const isTestEndClefStaffEntryBboxes = sampleFilename.startsWith(
|
||||
"test_end_measure_clefs_staffentry_bbox",
|
||||
);
|
||||
const isTestPageBreakImpliesSystemBreak = sampleFilename.startsWith(
|
||||
"test_pagebreak_implies_systembreak",
|
||||
);
|
||||
const isTestPageBottomMargin0 =
|
||||
sampleFilename.includes("PageBottomMargin0");
|
||||
const isTestTupletBracketTupletNumber = sampleFilename.includes(
|
||||
"test_tuplet_bracket_tuplet_number",
|
||||
);
|
||||
const isTestCajon2NoteSystem = sampleFilename.includes(
|
||||
"test_cajon_2-note-system",
|
||||
);
|
||||
isTestOctaveShiftInvisibleInstrument = sampleFilename.includes(
|
||||
"test_octaveshift_first_instrument_invisible",
|
||||
);
|
||||
const isTextOctaveShiftExtraGraphicalMeasure = sampleFilename.includes(
|
||||
"test_octaveshift_extragraphicalmeasure",
|
||||
);
|
||||
isTestInvisibleMeasureNotAffectingLayout = sampleFilename.includes(
|
||||
"test_invisible_measure_not_affecting_layout",
|
||||
);
|
||||
const isTestWedgeMultilineCrescendo = sampleFilename.includes(
|
||||
"test_wedge_multiline_crescendo",
|
||||
);
|
||||
const isTestWedgeMultilineDecrescendo = sampleFilename.includes(
|
||||
"test_wedge_multiline_decrescendo",
|
||||
);
|
||||
osmdInstance.EngravingRules.loadDefaultValues(); // note this may also be executed in setOptions below via drawingParameters default
|
||||
if (isTestEndClefStaffEntryBboxes) {
|
||||
drawBoundingBoxString = "VexFlowStaffEntry";
|
||||
} else {
|
||||
drawBoundingBoxString = options.boundingBoxes; // undefined is also a valid value: no bboxes
|
||||
}
|
||||
osmdInstance.setOptions({
|
||||
autoBeam: isFunctionTestAutobeam, // only set to true for function test autobeam
|
||||
coloringMode: isFunctionTestAutoColoring ? 2 : 0,
|
||||
// eslint-disable-next-line max-len
|
||||
coloringSetCustom: isFunctionTestAutoColoring
|
||||
? [
|
||||
"#d82c6b",
|
||||
"#F89D15",
|
||||
"#FFE21A",
|
||||
"#4dbd5c",
|
||||
"#009D96",
|
||||
"#43469d",
|
||||
"#76429c",
|
||||
"#ff0000",
|
||||
]
|
||||
: undefined,
|
||||
colorStemsLikeNoteheads: isFunctionTestAutoColoring,
|
||||
drawingParameters: defaultOrCompactTightMode, // note: default resets all EngravingRules. could be solved differently
|
||||
drawFromMeasureNumber: isFunctionTestDrawingRange ? 9 : 1,
|
||||
drawUpToMeasureNumber: isFunctionTestDrawingRange
|
||||
? 12
|
||||
: Number.MAX_SAFE_INTEGER,
|
||||
newSystemFromXML: isFunctionTestSystemAndPageBreaks,
|
||||
newSystemFromNewPageInXML: isTestPageBreakImpliesSystemBreak,
|
||||
newPageFromXML: isFunctionTestSystemAndPageBreaks,
|
||||
pageBackgroundColor: "#FFFFFF", // reset by drawingparameters default
|
||||
pageFormat: pageFormat, // reset by drawingparameters default,
|
||||
...makeSkyBottomLineOptions(),
|
||||
});
|
||||
// note that loadDefaultValues() may be executed in setOptions with drawingParameters default
|
||||
//osmdInstance.EngravingRules.RenderSingleHorizontalStaffline = true; // to use this option here, place it after setOptions(), see above
|
||||
osmdInstance.EngravingRules.AlwaysSetPreferredSkyBottomLineBackendAutomatically = false; // this would override the command line options (--plain etc)
|
||||
includeSkyBottomLine = options.skyBottomLine
|
||||
? options.skyBottomLine
|
||||
: false; // apparently es6 doesn't have ?? operator
|
||||
osmdInstance.drawSkyLine = includeSkyBottomLine; // if includeSkyBottomLine, draw skyline and bottomline, else not
|
||||
osmdInstance.drawBottomLine = includeSkyBottomLine;
|
||||
osmdInstance.setDrawBoundingBox(drawBoundingBoxString, false); // false: don't render (now). also (re-)set if undefined!
|
||||
if (isTestFlatBeams) {
|
||||
osmdInstance.EngravingRules.FlatBeams = true;
|
||||
// osmdInstance.EngravingRules.FlatBeamOffset = 30;
|
||||
osmdInstance.EngravingRules.FlatBeamOffset = 10;
|
||||
osmdInstance.EngravingRules.FlatBeamOffsetPerBeam = 10;
|
||||
} else {
|
||||
osmdInstance.EngravingRules.FlatBeams = false;
|
||||
}
|
||||
if (isTestPageBottomMargin0) {
|
||||
osmdInstance.EngravingRules.PageBottomMargin = 0;
|
||||
}
|
||||
if (isTestTupletBracketTupletNumber) {
|
||||
osmdInstance.EngravingRules.TupletNumberLimitConsecutiveRepetitions = true;
|
||||
osmdInstance.EngravingRules.TupletNumberMaxConsecutiveRepetitions = 2;
|
||||
osmdInstance.EngravingRules.TupletNumberAlwaysDisableAfterFirstMax = true; // necessary to trigger bug
|
||||
}
|
||||
if (isTestCajon2NoteSystem) {
|
||||
osmdInstance.EngravingRules.PercussionUseCajon2NoteSystem = true;
|
||||
}
|
||||
if (
|
||||
isTextOctaveShiftExtraGraphicalMeasure ||
|
||||
isTestOctaveShiftInvisibleInstrument ||
|
||||
isTestWedgeMultilineCrescendo ||
|
||||
isTestWedgeMultilineDecrescendo
|
||||
) {
|
||||
osmdInstance.EngravingRules.NewSystemAtXMLNewSystemAttribute = true;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
debug("loading sample " + sampleFilename, DEBUG);
|
||||
await osmdInstance.load(loadParameter, sampleFilename); // if using load.then() without await, memory will not be freed up between renders
|
||||
if (isTestOctaveShiftInvisibleInstrument) {
|
||||
osmdInstance.Sheet.Instruments[0].Visible = false;
|
||||
}
|
||||
if (isTestInvisibleMeasureNotAffectingLayout) {
|
||||
if (osmdInstance.Sheet.Instruments[1]) {
|
||||
// some systems can't handle ?. in this script (just a safety check anyways)
|
||||
osmdInstance.Sheet.Instruments[1].Visible = false;
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
debug(
|
||||
"couldn't load sample " + sampleFilename + ", skipping. Error: \n" + ex,
|
||||
);
|
||||
return Promise.reject(ex);
|
||||
}
|
||||
debug("xml loaded", DEBUG);
|
||||
try {
|
||||
osmdInstance.render();
|
||||
// there were reports that await could help here, but render isn't a synchronous function, and it seems to work. see #932
|
||||
} catch (ex) {
|
||||
debug("renderError: " + ex);
|
||||
}
|
||||
debug("rendered", DEBUG);
|
||||
|
||||
const markupStrings = []; // svg
|
||||
const dataUrls = []; // png
|
||||
let canvasImage;
|
||||
|
||||
// intended to use only for the chromacase partition use case (always 1 page in svg)
|
||||
let partitionDims = [-1, -1];
|
||||
|
||||
for (
|
||||
let pageNumber = 1;
|
||||
pageNumber < Number.POSITIVE_INFINITY;
|
||||
pageNumber++
|
||||
) {
|
||||
if (imageFormat === "png") {
|
||||
canvasImage = document.getElementById(
|
||||
"osmdCanvasVexFlowBackendCanvas" + pageNumber,
|
||||
);
|
||||
if (!canvasImage) {
|
||||
break;
|
||||
}
|
||||
if (!canvasImage.toDataURL) {
|
||||
debug(
|
||||
`error: could not get canvas image for page ${pageNumber} for file: ${sampleFilename}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
dataUrls.push(canvasImage.toDataURL());
|
||||
} else if (imageFormat === "svg") {
|
||||
const svgElement = document.getElementById("osmdSvgPage" + pageNumber);
|
||||
if (!svgElement) {
|
||||
break;
|
||||
}
|
||||
// The important xmlns attribute is not serialized unless we set it here
|
||||
svgElement.setAttribute("xmlns", "http://www.w3.org/2000/svg");
|
||||
const width = svgElement.getAttribute("width");
|
||||
const height = svgElement.getAttribute("height");
|
||||
partitionDims = [width, height];
|
||||
markupStrings.push(svgElement.outerHTML);
|
||||
}
|
||||
}
|
||||
|
||||
// create the cursor positions file
|
||||
getCursorPositions(osmdInstance, assetName, partitionDims);
|
||||
|
||||
for (
|
||||
let pageIndex = 0;
|
||||
pageIndex < Math.max(dataUrls.length, markupStrings.length);
|
||||
pageIndex++
|
||||
) {
|
||||
const pageNumberingString = `${pageIndex + 1}`;
|
||||
const skybottomlineString = includeSkyBottomLine ? "skybottomline_" : "";
|
||||
const graphicalNoteBboxesString = drawBoundingBoxString
|
||||
? "bbox" + drawBoundingBoxString + "_"
|
||||
: "";
|
||||
// pageNumberingString = dataUrls.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
|
||||
const pageFilename = `${imageDir}/${assetName}.${imageFormat}`;
|
||||
|
||||
if (imageFormat === "png") {
|
||||
const dataUrl = dataUrls[pageIndex];
|
||||
if (!dataUrl || !dataUrl.split) {
|
||||
debug(
|
||||
`error: could not get dataUrl (imageData) for page ${
|
||||
pageIndex + 1
|
||||
} of sample: ${sampleFilename}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const imageData = dataUrl.split(";base64,").pop();
|
||||
const imageBuffer = Buffer.from(imageData, "base64");
|
||||
|
||||
debug("got image data, saving to: " + pageFilename, DEBUG);
|
||||
FS.writeFileSync(pageFilename, imageBuffer, { encoding: "base64" });
|
||||
} else if (imageFormat === "svg") {
|
||||
const markup = markupStrings[pageIndex];
|
||||
if (!markup) {
|
||||
debug(
|
||||
`error: could not get markup (SVG data) for page ${
|
||||
pageIndex + 1
|
||||
} of sample: ${sampleFilename}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
debug("got svg markup data, saving to: " + pageFilename, DEBUG);
|
||||
// replace every bounding-box by none (react native doesn't support bounding-box)
|
||||
FS.writeFileSync(pageFilename, markup.replace(/bounding-box/g, "none"), {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
}
|
||||
|
||||
// debug: log memory usage
|
||||
// const usage = process.memoryUsage()
|
||||
// for (const entry of Object.entries(usage)) {
|
||||
// if (entry[0] === 'rss') {
|
||||
// if (entry[1] > maxRss) {
|
||||
// maxRss = entry[1]
|
||||
// maxRssFilename = pageFilename
|
||||
// }
|
||||
// }
|
||||
// debug(entry[0] + ': ' + entry[1] / (1024 * 1024) + 'mb')
|
||||
// }
|
||||
// debug('maxRss: ' + (maxRss / 1024 / 1024) + 'mb' + ' for ' + maxRssFilename)
|
||||
}
|
||||
// debug('maxRss total: ' + (maxRss / 1024 / 1024) + 'mb' + ' for ' + maxRssFilename)
|
||||
|
||||
// await sleep(5000)
|
||||
// }) // end read file
|
||||
}
|
||||
|
||||
function debug(msg, debugEnabled = true) {
|
||||
if (debugEnabled) {
|
||||
console.log("[generateImages] " + msg);
|
||||
}
|
||||
}
|
||||
|
||||
// init();
|
||||
@@ -65,7 +65,7 @@ async function bootstrap() {
|
||||
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
app.enableCors();
|
||||
app.useGlobalInterceptors(new AspectLogger());
|
||||
//app.useGlobalInterceptors(new AspectLogger());
|
||||
|
||||
await app.listen(3000);
|
||||
}
|
||||
|
||||
@@ -15,13 +15,14 @@ import {
|
||||
Req,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { CreateSongDto } from './dto/create-song.dto';
|
||||
import { SongService } from './song.service';
|
||||
import { Request } from 'express';
|
||||
import { Prisma, Song } from '@prisma/client';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
Header,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||
import { CreateSongDto } from "./dto/create-song.dto";
|
||||
import { SongService } from "./song.service";
|
||||
import { Request } from "express";
|
||||
import { Prisma, Song } from "@prisma/client";
|
||||
import { createReadStream, existsSync, readFileSync } from "fs";
|
||||
import {
|
||||
ApiNotFoundResponse,
|
||||
ApiOkResponse,
|
||||
@@ -29,14 +30,14 @@ import {
|
||||
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';
|
||||
} 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";
|
||||
|
||||
class SongHistoryResult {
|
||||
@ApiProperty()
|
||||
@@ -45,16 +46,16 @@ class SongHistoryResult {
|
||||
history: SongHistory[];
|
||||
}
|
||||
|
||||
@Controller('song')
|
||||
@ApiTags('song')
|
||||
@Controller("song")
|
||||
@ApiTags("song")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class SongController {
|
||||
static filterableFields: string[] = [
|
||||
'+id',
|
||||
'name',
|
||||
'+artistId',
|
||||
'+albumId',
|
||||
'+genreId',
|
||||
"+id",
|
||||
"name",
|
||||
"+artistId",
|
||||
"+albumId",
|
||||
"+genreId",
|
||||
];
|
||||
static includableFields: IncludeMap<Prisma.SongInclude> = {
|
||||
artist: true,
|
||||
@@ -69,36 +70,37 @@ export class SongController {
|
||||
private readonly historyService: HistoryService,
|
||||
) {}
|
||||
|
||||
@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' })
|
||||
async getMidi(@Param('id', ParseIntPipe) id: number) {
|
||||
@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" })
|
||||
async getMidi(@Param("id", ParseIntPipe) id: number) {
|
||||
const song = await this.songService.song({ id });
|
||||
if (!song) throw new NotFoundException('Song not found');
|
||||
if (!song) throw new NotFoundException("Song not found");
|
||||
|
||||
try {
|
||||
const file = createReadStream(song.midiPath);
|
||||
return new StreamableFile(file, { type: 'audio/midi' });
|
||||
return new StreamableFile(file, { type: "audio/midi" });
|
||||
} catch {
|
||||
throw new InternalServerErrorException();
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/illustration')
|
||||
@Get(":id/illustration")
|
||||
@ApiOperation({
|
||||
description: 'Streams the illustration of the requested song',
|
||||
description: "Streams the illustration of the requested song",
|
||||
})
|
||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
||||
@ApiOkResponse({ description: 'Returns the illustration succesfully' })
|
||||
@ApiNotFoundResponse({ description: "Song not found" })
|
||||
@ApiOkResponse({ description: "Returns the illustration succesfully" })
|
||||
@Header("Cache-Control", "max-age=86400")
|
||||
@Public()
|
||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
||||
async getIllustration(@Param("id", ParseIntPipe) id: number) {
|
||||
const song = await this.songService.song({ id });
|
||||
if (!song) throw new NotFoundException('Song not found');
|
||||
if (!song) throw new NotFoundException("Song not found");
|
||||
|
||||
if (song.illustrationPath === null) throw new NotFoundException();
|
||||
if (!existsSync(song.illustrationPath))
|
||||
throw new NotFoundException('Illustration not found');
|
||||
throw new NotFoundException("Illustration not found");
|
||||
|
||||
try {
|
||||
const file = createReadStream(song.illustrationPath);
|
||||
@@ -108,24 +110,77 @@ export class SongController {
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/musicXml')
|
||||
@Get(":id/musicXml")
|
||||
@ApiOperation({
|
||||
description: 'Streams the musicXML file of the requested song',
|
||||
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) {
|
||||
@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');
|
||||
if (!song) throw new NotFoundException("Song not found");
|
||||
|
||||
const file = createReadStream(song.musicXmlPath, { encoding: 'binary' });
|
||||
const file = createReadStream(song.musicXmlPath, { encoding: "binary" });
|
||||
return new StreamableFile(file);
|
||||
}
|
||||
|
||||
@Get(":id/assets/partition")
|
||||
@ApiOperation({
|
||||
description: "Streams the svg partition of the requested song",
|
||||
})
|
||||
@ApiNotFoundResponse({ description: "Song not found" })
|
||||
@ApiOkResponse({ description: "Returns the svg partition succesfully" })
|
||||
@Header("Cache-Control", "max-age=86400")
|
||||
@Header("Content-Type", "image/svg+xml")
|
||||
@Public()
|
||||
async getPartition(@Param("id", ParseIntPipe) id: number) {
|
||||
const song = await this.songService.song({ id });
|
||||
if (!song) throw new NotFoundException("Song not found");
|
||||
|
||||
// check if /data/cache/songs/id exists
|
||||
if (!existsSync("/data/cache/songs/" + id + ".svg")) {
|
||||
// if not, generate assets
|
||||
await this.songService.createAssets(song.musicXmlPath, id);
|
||||
}
|
||||
|
||||
try {
|
||||
const file = readFileSync("/data/cache/songs/" + id + ".svg");
|
||||
return file.toString();
|
||||
} catch {
|
||||
throw new InternalServerErrorException();
|
||||
}
|
||||
}
|
||||
|
||||
@Get(":id/assets/cursors")
|
||||
@ApiOperation({
|
||||
description: "Streams the partition cursors of the requested song",
|
||||
})
|
||||
@ApiNotFoundResponse({ description: "Song not found" })
|
||||
@ApiOkResponse({ description: "Returns the partition cursors succesfully" })
|
||||
@Header("Cache-Control", "max-age=86400")
|
||||
@Header("Content-Type", "application/json")
|
||||
async getCursors(@Param("id", ParseIntPipe) id: number) {
|
||||
const song = await this.songService.song({ id });
|
||||
if (!song) throw new NotFoundException("Song not found");
|
||||
|
||||
// check if /data/cache/songs/id exists
|
||||
if (!existsSync("/data/cache/songs/" + id + ".json")) {
|
||||
// if not, generate assets
|
||||
await this.songService.createAssets(song.musicXmlPath, id);
|
||||
}
|
||||
|
||||
try {
|
||||
const file = readFileSync("/data/cache/songs/" + id + ".json");
|
||||
return JSON.parse(file.toString());
|
||||
} catch {
|
||||
throw new InternalServerErrorException();
|
||||
}
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({
|
||||
description:
|
||||
'register a new song in the database, should not be used by the frontend',
|
||||
"register a new song in the database, should not be used by the frontend",
|
||||
})
|
||||
async create(@Body() createSongDto: CreateSongDto) {
|
||||
try {
|
||||
@@ -148,13 +203,13 @@ export class SongController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ description: 'delete a song by id' })
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
@Delete(":id")
|
||||
@ApiOperation({ description: "delete a song by id" })
|
||||
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.songService.deleteSong({ id });
|
||||
} catch {
|
||||
throw new NotFoundException('Invalid ID');
|
||||
throw new NotFoundException("Invalid ID");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,9 +218,9 @@ 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,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Song>> {
|
||||
const ret = await this.songService.songs({
|
||||
skip,
|
||||
@@ -176,14 +231,14 @@ export class SongController {
|
||||
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' })
|
||||
@Get(":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,
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Query("include") include: string,
|
||||
) {
|
||||
const res = await this.songService.song(
|
||||
{
|
||||
@@ -192,21 +247,22 @@ export class SongController {
|
||||
mapInclude(include, req, SongController.includableFields),
|
||||
);
|
||||
|
||||
if (res === null) throw new NotFoundException('Song not found');
|
||||
if (res === null) throw new NotFoundException("Song not found");
|
||||
return res;
|
||||
}
|
||||
|
||||
@Get(':id/history')
|
||||
@Get(":id/history")
|
||||
@HttpCode(200)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({
|
||||
description: 'get the history of the connected user on a specific song',
|
||||
description: "get the history of the connected user on a specific song",
|
||||
})
|
||||
@ApiOkResponse({
|
||||
type: SongHistoryResult,
|
||||
description: 'Records of previous games of the user',
|
||||
description: "Records of previous games of the user",
|
||||
})
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
async getHistory(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
async getHistory(@Req() req: any, @Param("id", ParseIntPipe) id: number) {
|
||||
return this.historyService.getForSong({
|
||||
playerId: req.user.id,
|
||||
songId: id,
|
||||
|
||||
@@ -1,10 +1,29 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Song } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, Song } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { generateSongAssets } from "src/assetsgenerator/generateImages_browserless";
|
||||
|
||||
@Injectable()
|
||||
export class SongService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
// number is the song id
|
||||
private assetCreationTasks: Map<number, Promise<void>>;
|
||||
constructor(private prisma: PrismaService) {
|
||||
this.assetCreationTasks = new Map();
|
||||
}
|
||||
|
||||
async createAssets(mxlPath: string, songId: number): Promise<void> {
|
||||
if (this.assetCreationTasks.has(songId)) {
|
||||
await this.assetCreationTasks.get(songId);
|
||||
this.assetCreationTasks.delete(songId);
|
||||
return;
|
||||
}
|
||||
// mxlPath can the path to an archive to an xml file or the path to the xml file directly
|
||||
this.assetCreationTasks.set(
|
||||
songId,
|
||||
generateSongAssets(songId, mxlPath, "/data/cache/songs", "svg"),
|
||||
);
|
||||
return await this.assetCreationTasks.get(songId);
|
||||
}
|
||||
|
||||
async songByArtist(data: number): Promise<Song[]> {
|
||||
return this.prisma.song.findMany({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"module": "NodeNext",
|
||||
"declaration": true,
|
||||
"removeComments": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
@@ -16,6 +16,9 @@
|
||||
"noImplicitAny": false,
|
||||
"strictBindCallApply": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"allowJs": true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ volumes:
|
||||
|
||||
services:
|
||||
back:
|
||||
#platform: linux/amd64
|
||||
build:
|
||||
context: ./back
|
||||
dockerfile: Dockerfile.dev
|
||||
|
||||
@@ -9,6 +9,7 @@ volumes:
|
||||
|
||||
services:
|
||||
back:
|
||||
#platform: linux/amd64
|
||||
build: ./back
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
29
front/API.ts
29
front/API.ts
@@ -23,6 +23,7 @@ import { AccessTokenResponseHandler } from './models/AccessTokenResponse';
|
||||
import * as yup from 'yup';
|
||||
import { base64ToBlob } from './utils/base64ToBlob';
|
||||
import { ImagePickerAsset } from 'expo-image-picker';
|
||||
import { SongCursorInfos, SongCursorInfosHandler } from './models/SongCursorInfos';
|
||||
|
||||
type AuthenticationInput = { username: string; password: string };
|
||||
type RegistrationInput = AuthenticationInput & { email: string };
|
||||
@@ -66,7 +67,9 @@ export class ValidationError extends Error {
|
||||
|
||||
export default class API {
|
||||
public static readonly baseUrl =
|
||||
Platform.OS === 'web' ? '/api' : process.env.EXPO_PUBLIC_API_URL!;
|
||||
Platform.OS === 'web' && !process.env.EXPO_PUBLIC_API_URL
|
||||
? '/api'
|
||||
: process.env.EXPO_PUBLIC_API_URL!;
|
||||
public static async fetch(
|
||||
params: FetchParams,
|
||||
handle: Pick<Required<HandleParams>, 'raw'>
|
||||
@@ -113,11 +116,11 @@ export default class API {
|
||||
}
|
||||
const handler = handle.handler;
|
||||
const body = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new APIError(response.statusText ?? body, response.status, 'unknownError');
|
||||
}
|
||||
try {
|
||||
const jsonResponse = JSON.parse(body);
|
||||
if (!response.ok) {
|
||||
throw new APIError(response.statusText ?? body, response.status, 'unknownError');
|
||||
}
|
||||
const validated = await handler.validator.validate(jsonResponse).catch((e) => {
|
||||
if (e instanceof yup.ValidationError) {
|
||||
console.error(e, 'Got: ' + body);
|
||||
@@ -150,7 +153,6 @@ export default class API {
|
||||
/// We want that 401 error to be thrown, instead of the plain validation vone
|
||||
if (e.status == 401)
|
||||
throw new APIError('invalidCredentials', 401, 'invalidCredentials');
|
||||
if (!(e instanceof APIError)) throw e;
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
@@ -716,4 +718,21 @@ export default class API {
|
||||
),
|
||||
};
|
||||
}
|
||||
public static getSongCursorInfos(songId: number): Query<SongCursorInfos> {
|
||||
return {
|
||||
key: ['cursorInfos', songId],
|
||||
exec: () => {
|
||||
return API.fetch(
|
||||
{
|
||||
route: `/song/${songId}/assets/cursors`,
|
||||
},
|
||||
{ handler: SongCursorInfosHandler }
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static getPartitionSvgUrl(songId: number): string {
|
||||
return `${API.baseUrl}/song/${songId}/assets/partition`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ COPY . .
|
||||
ARG API_URL
|
||||
ENV EXPO_PUBLIC_API_URL=$API_URL
|
||||
ARG SCORO_URL
|
||||
ENV EXPO_PUBLIC_API_URL=$SCORO_URL
|
||||
ENV EXPO_PUBLIC_SCORO_URL=$SCORO_URL
|
||||
|
||||
RUN yarn tsc && npx expo export:web
|
||||
|
||||
|
||||
@@ -10,11 +10,8 @@ import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/
|
||||
import { RootState, useSelector } from './state/Store';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { Translate, translate } from './i18n/i18n';
|
||||
import SongLobbyView from './views/SongLobbyView';
|
||||
import StartPageView from './views/StartPageView';
|
||||
import HomeView from './views/HomeView';
|
||||
import SearchView from './views/SearchView';
|
||||
import SetttingsNavigator from './views/settings/SettingsView';
|
||||
import SettingsTab from './views/settings/SettingsView';
|
||||
import { useQuery } from './Queries';
|
||||
import API, { APIError } from './API';
|
||||
import PlayView from './views/PlayView';
|
||||
@@ -32,10 +29,11 @@ import GoogleView from './views/GoogleView';
|
||||
import VerifiedView from './views/VerifiedView';
|
||||
import SigninView from './views/SigninView';
|
||||
import SignupView from './views/SignupView';
|
||||
import TabNavigation from './components/V2/TabNavigation';
|
||||
import PasswordResetView from './views/PasswordResetView';
|
||||
import ForgotPasswordView from './views/ForgotPasswordView';
|
||||
import Leaderboardiew from './views/LeaderboardView';
|
||||
import DiscoveryView from './views/V2/DiscoveryView';
|
||||
import MusicView from './views/MusicView';
|
||||
|
||||
// Util function to hide route props in URL
|
||||
const removeMe = () => '';
|
||||
@@ -43,29 +41,33 @@ const removeMe = () => '';
|
||||
const protectedRoutes = () =>
|
||||
({
|
||||
Home: {
|
||||
component: HomeView,
|
||||
options: { title: translate('welcome'), headerLeft: null },
|
||||
component: DiscoveryView,
|
||||
options: { headerShown: false },
|
||||
link: '/',
|
||||
},
|
||||
Music: {
|
||||
component: MusicView,
|
||||
options: { headerShown: false },
|
||||
link: '/music',
|
||||
},
|
||||
HomeNew: {
|
||||
component: TabNavigation,
|
||||
component: DiscoveryView,
|
||||
options: { headerShown: false },
|
||||
link: '/V2',
|
||||
},
|
||||
Play: { component: PlayView, options: { title: translate('play') }, link: '/play/:songId' },
|
||||
Play: {
|
||||
component: PlayView,
|
||||
options: { headerShown: false, title: translate('play') },
|
||||
link: '/play/:songId',
|
||||
},
|
||||
Settings: {
|
||||
component: SetttingsNavigator,
|
||||
options: { title: 'Settings' },
|
||||
component: SettingsTab,
|
||||
options: { headerShown: false },
|
||||
link: '/settings/:screen?',
|
||||
stringify: {
|
||||
screen: removeMe,
|
||||
},
|
||||
},
|
||||
Song: {
|
||||
component: SongLobbyView,
|
||||
options: { title: translate('play') },
|
||||
link: '/song/:songId',
|
||||
},
|
||||
Artist: {
|
||||
component: ArtistDetailsView,
|
||||
options: { title: translate('artistFilter') },
|
||||
@@ -83,7 +85,7 @@ const protectedRoutes = () =>
|
||||
},
|
||||
Search: {
|
||||
component: SearchView,
|
||||
options: { title: translate('search') },
|
||||
options: { headerShown: false },
|
||||
link: '/search/:query?',
|
||||
},
|
||||
Leaderboard: {
|
||||
@@ -96,7 +98,7 @@ const protectedRoutes = () =>
|
||||
options: { title: translate('error'), headerLeft: null },
|
||||
link: undefined,
|
||||
},
|
||||
User: { component: ProfileView, options: { title: translate('user') }, link: '/user' },
|
||||
User: { component: ProfileView, options: { headerShown: false }, link: '/user' },
|
||||
Verified: {
|
||||
component: VerifiedView,
|
||||
options: { title: 'Verify email', headerShown: false },
|
||||
@@ -106,11 +108,6 @@ const protectedRoutes = () =>
|
||||
|
||||
const publicRoutes = () =>
|
||||
({
|
||||
Start: {
|
||||
component: StartPageView,
|
||||
options: { title: 'Chromacase', headerShown: false },
|
||||
link: '/',
|
||||
},
|
||||
Login: {
|
||||
component: SigninView,
|
||||
options: { title: translate('signInBtn'), headerShown: false },
|
||||
@@ -217,7 +214,7 @@ const ProfileErrorView = (props: { onTryAgain: () => void }) => {
|
||||
<TextButton
|
||||
onPress={() => {
|
||||
dispatch(unsetAccessToken());
|
||||
navigation.navigate('Start');
|
||||
navigation.navigate('Login');
|
||||
}}
|
||||
colorScheme="error"
|
||||
variant="outline"
|
||||
|
||||
@@ -4,9 +4,39 @@ import { useEffect } from 'react';
|
||||
|
||||
const ThemeProvider = ({ children }: { children: JSX.Element }) => {
|
||||
const colorScheme = useColorScheme();
|
||||
const lightGlassmorphism = {
|
||||
50: 'rgba(255,255,255,0.9)',
|
||||
100: 'rgba(255,255,255,0.1)',
|
||||
200: 'rgba(255,255,255,0.2)',
|
||||
300: 'rgba(255,255,255,0.3)',
|
||||
400: 'rgba(255,255,255,0.4)',
|
||||
500: 'rgba(255,255,255,0.5)',
|
||||
600: 'rgba(255,255,255,0.6)',
|
||||
700: 'rgba(255,255,255,0.7)',
|
||||
800: 'rgba(255,255,255,0.8)',
|
||||
900: 'rgba(255,255,255,0.9)',
|
||||
1000: 'rgba(255,255,255,1)',
|
||||
};
|
||||
const darkGlassmorphism = {
|
||||
50: 'rgba(16,16,20,0.9)',
|
||||
100: 'rgba(16,16,20,0.1)',
|
||||
200: 'rgba(16,16,20,0.2)',
|
||||
300: 'rgba(16,16,20,0.3)',
|
||||
400: 'rgba(16,16,20,0.4)',
|
||||
500: 'rgba(16,16,20,0.5)',
|
||||
600: 'rgba(16,16,20,0.6)',
|
||||
700: 'rgba(16,16,20,0.7)',
|
||||
800: 'rgba(16,16,20,0.8)',
|
||||
900: 'rgba(16,16,20,0.9)',
|
||||
1000: 'rgba(16,16,20,1)',
|
||||
};
|
||||
|
||||
const glassmorphism = colorScheme === 'light' ? lightGlassmorphism : darkGlassmorphism;
|
||||
const text = colorScheme === 'light' ? darkGlassmorphism : lightGlassmorphism;
|
||||
|
||||
return (
|
||||
<NativeBaseProvider
|
||||
isSSR={false}
|
||||
theme={extendTheme({
|
||||
config: {
|
||||
useSystemColorMode: false,
|
||||
@@ -18,6 +48,7 @@ const ThemeProvider = ({ children }: { children: JSX.Element }) => {
|
||||
mono: 'Lexend',
|
||||
},
|
||||
colors: {
|
||||
text: text,
|
||||
primary: {
|
||||
50: '#eff1fe',
|
||||
100: '#e7eafe',
|
||||
@@ -102,6 +133,7 @@ const ThemeProvider = ({ children }: { children: JSX.Element }) => {
|
||||
800: '#6b2124',
|
||||
900: '#531a1c',
|
||||
},
|
||||
coolGray: glassmorphism,
|
||||
},
|
||||
components: {
|
||||
Button: {
|
||||
|
||||
@@ -81,9 +81,9 @@ android {
|
||||
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
||||
namespace 'build.apk'
|
||||
namespace 'com.arthichaud.Chromacase'
|
||||
defaultConfig {
|
||||
applicationId 'build.apk'
|
||||
applicationId 'com.arthichaud.Chromacase'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.arthichaud.Chromacase;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.flipper.android.AndroidFlipperClient;
|
||||
import com.facebook.flipper.android.utils.FlipperUtils;
|
||||
import com.facebook.flipper.core.FlipperClient;
|
||||
import com.facebook.flipper.plugins.crashreporter.CrashReporterPlugin;
|
||||
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.fresco.FrescoFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
|
||||
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.network.FlipperOkhttpInterceptor;
|
||||
import com.facebook.flipper.plugins.network.NetworkFlipperPlugin;
|
||||
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
|
||||
import com.facebook.react.ReactInstanceEventListener;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.modules.network.NetworkingModule;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* Class responsible of loading Flipper inside your React Native application. This is the debug
|
||||
* flavor of it. Here you can add your own plugins and customize the Flipper setup.
|
||||
*/
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
if (FlipperUtils.shouldEnableFlipper(context)) {
|
||||
final FlipperClient client = AndroidFlipperClient.getInstance(context);
|
||||
|
||||
client.addPlugin(new InspectorFlipperPlugin(context, DescriptorMapping.withDefaults()));
|
||||
client.addPlugin(new DatabasesFlipperPlugin(context));
|
||||
client.addPlugin(new SharedPreferencesFlipperPlugin(context));
|
||||
client.addPlugin(CrashReporterPlugin.getInstance());
|
||||
|
||||
NetworkFlipperPlugin networkFlipperPlugin = new NetworkFlipperPlugin();
|
||||
NetworkingModule.setCustomClientBuilder(
|
||||
new NetworkingModule.CustomClientBuilder() {
|
||||
@Override
|
||||
public void apply(OkHttpClient.Builder builder) {
|
||||
builder.addNetworkInterceptor(new FlipperOkhttpInterceptor(networkFlipperPlugin));
|
||||
}
|
||||
});
|
||||
client.addPlugin(networkFlipperPlugin);
|
||||
client.start();
|
||||
|
||||
// Fresco Plugin needs to ensure that ImagePipelineFactory is initialized
|
||||
// Hence we run if after all native modules have been initialized
|
||||
ReactContext reactContext = reactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext == null) {
|
||||
reactInstanceManager.addReactInstanceEventListener(
|
||||
new ReactInstanceEventListener() {
|
||||
@Override
|
||||
public void onReactContextInitialized(ReactContext reactContext) {
|
||||
reactInstanceManager.removeReactInstanceEventListener(this);
|
||||
reactContext.runOnNativeModulesQueueThread(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
client.addPlugin(new FrescoFlipperPlugin());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
|
||||
<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"/>
|
||||
@@ -11,7 +13,7 @@
|
||||
<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">
|
||||
<application android:name=".MainApplication" android:label="@string/app_name" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" 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"/>
|
||||
@@ -25,7 +27,8 @@
|
||||
<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"/>
|
||||
<data android:scheme="com.arthichaud.Chromacase"/>
|
||||
<data android:scheme="exp+chromacase"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity android:name="com.facebook.react.devsupport.DevSettingsActivity" android:exported="false"/>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.arthichaud.Chromacase;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.facebook.react.ReactActivity;
|
||||
import com.facebook.react.ReactActivityDelegate;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactActivityDelegate;
|
||||
|
||||
import expo.modules.ReactActivityDelegateWrapper;
|
||||
|
||||
public class MainActivity extends ReactActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// Set the theme to AppTheme BEFORE onCreate to support
|
||||
// coloring the background, status bar, and navigation bar.
|
||||
// This is required for expo-splash-screen.
|
||||
setTheme(R.style.AppTheme);
|
||||
super.onCreate(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript.
|
||||
* This is used to schedule rendering of the component.
|
||||
*/
|
||||
@Override
|
||||
protected String getMainComponentName() {
|
||||
return "main";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the instance of the {@link ReactActivityDelegate}. Here we use a util class {@link
|
||||
* DefaultReactActivityDelegate} which allows you to easily enable Fabric and Concurrent React
|
||||
* (aka React 18) with two boolean flags.
|
||||
*/
|
||||
@Override
|
||||
protected ReactActivityDelegate createReactActivityDelegate() {
|
||||
return new ReactActivityDelegateWrapper(this, BuildConfig.IS_NEW_ARCHITECTURE_ENABLED, new DefaultReactActivityDelegate(
|
||||
this,
|
||||
getMainComponentName(),
|
||||
// If you opted-in for the New Architecture, we enable the Fabric Renderer.
|
||||
DefaultNewArchitectureEntryPoint.getFabricEnabled()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Align the back button behavior with Android S
|
||||
* where moving root activities to background instead of finishing activities.
|
||||
* @see <a href="https://developer.android.com/reference/android/app/Activity#onBackPressed()">onBackPressed</a>
|
||||
*/
|
||||
@Override
|
||||
public void invokeDefaultOnBackPressed() {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
|
||||
if (!moveTaskToBack(false)) {
|
||||
// For non-root activities, use the default implementation to finish them.
|
||||
super.invokeDefaultOnBackPressed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the default back button implementation on Android S
|
||||
// because it's doing more than {@link Activity#moveTaskToBack} in fact.
|
||||
super.invokeDefaultOnBackPressed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.arthichaud.Chromacase;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.res.Configuration;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.facebook.react.PackageList;
|
||||
import com.facebook.react.ReactApplication;
|
||||
import com.facebook.react.ReactNativeHost;
|
||||
import com.facebook.react.ReactPackage;
|
||||
import com.facebook.react.config.ReactFeatureFlags;
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint;
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost;
|
||||
import com.facebook.soloader.SoLoader;
|
||||
|
||||
import expo.modules.ApplicationLifecycleDispatcher;
|
||||
import expo.modules.ReactNativeHostWrapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MainApplication extends Application implements ReactApplication {
|
||||
|
||||
private final ReactNativeHost mReactNativeHost =
|
||||
new ReactNativeHostWrapper(this, new DefaultReactNativeHost(this) {
|
||||
@Override
|
||||
public boolean getUseDeveloperSupport() {
|
||||
return BuildConfig.DEBUG;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<ReactPackage> getPackages() {
|
||||
@SuppressWarnings("UnnecessaryLocalVariable")
|
||||
List<ReactPackage> packages = new PackageList(this).getPackages();
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// packages.add(new MyReactNativePackage());
|
||||
return packages;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getJSMainModuleName() {
|
||||
return ".expo/.virtual-metro-entry";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isNewArchEnabled() {
|
||||
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean isHermesEnabled() {
|
||||
return BuildConfig.IS_HERMES_ENABLED;
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public ReactNativeHost getReactNativeHost() {
|
||||
return mReactNativeHost;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
SoLoader.init(this, /* native exopackage */ false);
|
||||
if (!BuildConfig.REACT_NATIVE_UNSTABLE_USE_RUNTIME_SCHEDULER_ALWAYS) {
|
||||
ReactFeatureFlags.unstable_useRuntimeSchedulerAlways = false;
|
||||
}
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
DefaultNewArchitectureEntryPoint.load();
|
||||
}
|
||||
ReactNativeFlipper.initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
|
||||
ApplicationLifecycleDispatcher.onApplicationCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
ApplicationLifecycleDispatcher.onConfigurationChanged(this, newConfig);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/iconBackground"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/iconBackground"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,6 +1,6 @@
|
||||
<resources>
|
||||
<color name="splashscreen_background">#ffffff</color>
|
||||
<color name="iconBackground">#FFFFFF</color>
|
||||
<color name="iconBackground">#ffffff</color>
|
||||
<color name="colorPrimary">#023c69</color>
|
||||
<color name="colorPrimaryDark">#ffffff</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* <p>This source code is licensed under the MIT license found in the LICENSE file in the root
|
||||
* directory of this source tree.
|
||||
*/
|
||||
package com.arthichaud.Chromacase;
|
||||
|
||||
import android.content.Context;
|
||||
import com.facebook.react.ReactInstanceManager;
|
||||
|
||||
/**
|
||||
* Class responsible of loading Flipper inside your React Native application. This is the release
|
||||
* flavor of it so it's empty as we don't want to load Flipper.
|
||||
*/
|
||||
public class ReactNativeFlipper {
|
||||
public static void initializeFlipper(Context context, ReactInstanceManager reactInstanceManager) {
|
||||
// Do nothing as we don't want to initialize Flipper on Release.
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,9 @@
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
},
|
||||
"permissions": ["android.permission.RECORD_AUDIO"],
|
||||
"package": "com.arthichaud.Chromacase"
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
|
||||
BIN
front/assets/piano/a0.mp3
Normal file
BIN
front/assets/piano/a0.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/a1.mp3
Normal file
BIN
front/assets/piano/a1.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/a2.mp3
Normal file
BIN
front/assets/piano/a2.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/a3.mp3
Normal file
BIN
front/assets/piano/a3.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/a4.mp3
Normal file
BIN
front/assets/piano/a4.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/a5.mp3
Normal file
BIN
front/assets/piano/a5.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/a6.mp3
Normal file
BIN
front/assets/piano/a6.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/a7.mp3
Normal file
BIN
front/assets/piano/a7.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/ab1.mp3
Normal file
BIN
front/assets/piano/ab1.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/ab2.mp3
Normal file
BIN
front/assets/piano/ab2.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/ab3.mp3
Normal file
BIN
front/assets/piano/ab3.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/ab4.mp3
Normal file
BIN
front/assets/piano/ab4.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/ab5.mp3
Normal file
BIN
front/assets/piano/ab5.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/ab6.mp3
Normal file
BIN
front/assets/piano/ab6.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/ab7.mp3
Normal file
BIN
front/assets/piano/ab7.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/b0.mp3
Normal file
BIN
front/assets/piano/b0.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/b1.mp3
Normal file
BIN
front/assets/piano/b1.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/b2.mp3
Normal file
BIN
front/assets/piano/b2.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/b3.mp3
Normal file
BIN
front/assets/piano/b3.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/b4.mp3
Normal file
BIN
front/assets/piano/b4.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/b5.mp3
Normal file
BIN
front/assets/piano/b5.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/b6.mp3
Normal file
BIN
front/assets/piano/b6.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/b7.mp3
Normal file
BIN
front/assets/piano/b7.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/bb0.mp3
Normal file
BIN
front/assets/piano/bb0.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/bb1.mp3
Normal file
BIN
front/assets/piano/bb1.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/bb2.mp3
Normal file
BIN
front/assets/piano/bb2.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/bb3.mp3
Normal file
BIN
front/assets/piano/bb3.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/bb4.mp3
Normal file
BIN
front/assets/piano/bb4.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/bb5.mp3
Normal file
BIN
front/assets/piano/bb5.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/bb6.mp3
Normal file
BIN
front/assets/piano/bb6.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/bb7.mp3
Normal file
BIN
front/assets/piano/bb7.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/c1.mp3
Normal file
BIN
front/assets/piano/c1.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/c2.mp3
Normal file
BIN
front/assets/piano/c2.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/c3.mp3
Normal file
BIN
front/assets/piano/c3.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/c4.mp3
Normal file
BIN
front/assets/piano/c4.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/c5.mp3
Normal file
BIN
front/assets/piano/c5.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/c6.mp3
Normal file
BIN
front/assets/piano/c6.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/c7.mp3
Normal file
BIN
front/assets/piano/c7.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/c8.mp3
Normal file
BIN
front/assets/piano/c8.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/d1.mp3
Normal file
BIN
front/assets/piano/d1.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/d2.mp3
Normal file
BIN
front/assets/piano/d2.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/d3.mp3
Normal file
BIN
front/assets/piano/d3.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/d4.mp3
Normal file
BIN
front/assets/piano/d4.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/d5.mp3
Normal file
BIN
front/assets/piano/d5.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/d6.mp3
Normal file
BIN
front/assets/piano/d6.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/d7.mp3
Normal file
BIN
front/assets/piano/d7.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/db1.mp3
Normal file
BIN
front/assets/piano/db1.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/db2.mp3
Normal file
BIN
front/assets/piano/db2.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/db3.mp3
Normal file
BIN
front/assets/piano/db3.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/db4.mp3
Normal file
BIN
front/assets/piano/db4.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/db5.mp3
Normal file
BIN
front/assets/piano/db5.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/db6.mp3
Normal file
BIN
front/assets/piano/db6.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/db7.mp3
Normal file
BIN
front/assets/piano/db7.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/e1.mp3
Normal file
BIN
front/assets/piano/e1.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/e2.mp3
Normal file
BIN
front/assets/piano/e2.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/e3.mp3
Normal file
BIN
front/assets/piano/e3.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/e4.mp3
Normal file
BIN
front/assets/piano/e4.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/e5.mp3
Normal file
BIN
front/assets/piano/e5.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/e6.mp3
Normal file
BIN
front/assets/piano/e6.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/e7.mp3
Normal file
BIN
front/assets/piano/e7.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/eb1.mp3
Normal file
BIN
front/assets/piano/eb1.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/eb2.mp3
Normal file
BIN
front/assets/piano/eb2.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/eb3.mp3
Normal file
BIN
front/assets/piano/eb3.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/eb4.mp3
Normal file
BIN
front/assets/piano/eb4.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/eb5.mp3
Normal file
BIN
front/assets/piano/eb5.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/eb6.mp3
Normal file
BIN
front/assets/piano/eb6.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/eb7.mp3
Normal file
BIN
front/assets/piano/eb7.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/f1.mp3
Normal file
BIN
front/assets/piano/f1.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/f2.mp3
Normal file
BIN
front/assets/piano/f2.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/f3.mp3
Normal file
BIN
front/assets/piano/f3.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/f4.mp3
Normal file
BIN
front/assets/piano/f4.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/f5.mp3
Normal file
BIN
front/assets/piano/f5.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/f6.mp3
Normal file
BIN
front/assets/piano/f6.mp3
Normal file
Binary file not shown.
BIN
front/assets/piano/f7.mp3
Normal file
BIN
front/assets/piano/f7.mp3
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user