Merge branch 'main' into feat/adc/search-view-v2

This commit is contained in:
danis
2023-11-30 13:41:27 +01:00
207 changed files with 9067 additions and 6010 deletions

View File

@@ -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:

View File

@@ -1,4 +1,4 @@
{
"singleQuote": true,
"singleQuote": false,
"trailingComma": "all"
}

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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$",

View 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();

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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({

View File

@@ -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,
}
}

View File

@@ -7,6 +7,7 @@ volumes:
services:
back:
#platform: linux/amd64
build:
context: ./back
dockerfile: Dockerfile.dev

View File

@@ -9,6 +9,7 @@ volumes:
services:
back:
#platform: linux/amd64
build: ./back
ports:
- "3000:3000"

View File

@@ -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`;
}
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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: {

View File

@@ -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

View File

@@ -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());
}
}
}
}

View File

@@ -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"/>

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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.
}
}

View File

@@ -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

Binary file not shown.

BIN
front/assets/piano/a1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/a2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/a3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/a4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/a5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/a6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/a7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/ab1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/ab2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/ab3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/ab4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/ab5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/ab6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/ab7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b0.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb0.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c8.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/d1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/d2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/d3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/d4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/d5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/d6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/d7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/db1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/db2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/db3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/db4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/db5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/db6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/db7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/e1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/e2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/e3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/e4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/e5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/e6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/e7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/eb1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/eb2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/eb3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/eb4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/eb5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/eb6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/eb7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/f1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/f2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/f3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/f4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/f5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/f6.mp3 Normal file

Binary file not shown.

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