diff --git a/.firebaserc b/.firebaserc index 60920e0..755241e 100644 --- a/.firebaserc +++ b/.firebaserc @@ -1,6 +1,7 @@ { "projects": { - "roundaround": "roundaround" + "roundaround": "roundaround", + "default": "roundaround" }, "targets": { "roundaround": { @@ -17,4 +18,4 @@ } } } -} \ No newline at end of file +} diff --git a/.github/workflows/firebase-hosting-merge.yml b/.github/workflows/firebase-hosting-merge.yml new file mode 100644 index 0000000..ed79aab --- /dev/null +++ b/.github/workflows/firebase-hosting-merge.yml @@ -0,0 +1,22 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on merge +'on': + push: + branches: + - master +jobs: + build_and_deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: | + yarn install + yarn build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_ROUNDAROUND }}' + channelId: live + projectId: roundaround diff --git a/.github/workflows/firebase-hosting-pull-request.yml b/.github/workflows/firebase-hosting-pull-request.yml new file mode 100644 index 0000000..0ffb691 --- /dev/null +++ b/.github/workflows/firebase-hosting-pull-request.yml @@ -0,0 +1,19 @@ +# This file was auto-generated by the Firebase CLI +# https://github.com/firebase/firebase-tools + +name: Deploy to Firebase Hosting on PR +'on': pull_request +jobs: + build_and_preview: + if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - run: | + yarn install + yarn build + - uses: FirebaseExtended/action-hosting-deploy@v0 + with: + repoToken: '${{ secrets.GITHUB_TOKEN }}' + firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_ROUNDAROUND }}' + projectId: roundaround diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..66ff04c --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,26 @@ +name: End-to-end tests +on: [push, pull_request] +jobs: + cypress-eslint-run: + runs-on: ubuntu-20.04 + # env: + # CYPRESS_BASE_URL: https://roundaround-stage.web.app/ + steps: + - name: Checkout + uses: actions/checkout@v2 + # Install NPM dependencies, cache them correctlyand run all Cypress tests + - name: set master url + if: github.ref == 'refs/heads/master' + run: CYPRESS_BASE_URL=http://rounds.studio + - name: set stage url + if: github.ref == 'refs/heads/stage' + run: CYPRESS_BASE_URL=https://roundaround-stage.web.app/ + - name: set dev url + if: github.ref != 'refs/heads/stage' && github.ref != 'refs/heads/master' + run: CYPRESS_BASE_URL=https://roundaround-dev.web.app/ + - name: Cypress run + uses: cypress-io/github-action@v2 + #uses: actions/download-artifact@v2 + # Run Eslint checks + - name: Eslint run + run: ./node_modules/.bin/eslint --ext js,jsx src diff --git a/.gitignore b/.gitignore index 8c69b25..10f9b39 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,13 @@ ref functions/node_modules -.eslintcache \ No newline at end of file +.eslintcache + +# Ignore IntelliJ generated files +.idea + +# Cypress test results +cypress/screenshots +cypress/videos +cypress/results +cypress/fixtures/example.json diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..1116b6a --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ +#!/bin/sh +./node_modules/.bin/eslint --ext js,jsx src diff --git a/README.md b/README.md index 78be71f..3740921 100644 --- a/README.md +++ b/README.md @@ -17,23 +17,86 @@ Reference: - Simple radial multi-layer simple sequencer: https://tylerbisson.com/Groove-Pizzeria/ - Another simple implementation: https://github.com/NYUMusEdLab/Accessible-Groove-Pizza -## Musical Concepts -![Step Sequencers: Traditional (Linear) and Radial Metaphors](/docs/images/RoundAround_StepSequencers.jpeg) -## Data Concepts -![Key Data Concepts for RoundAround](/docs/images/RoundAround_Concepts.jpeg) +# Development +## Set Node Version +Please use node v14.17.6 - the latest stable version of node, [nvm](https://tecadmin.net/install-nvm-macos-with-homebrew/) is an easy way to do this -## Local Development -- `yarn` -- `yarn start` +On OX, install Homebrew if you don't have it: +``` +ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" +``` + +Install and select said version of Node: +``` +nvm install 14.17.6 +nvm use v14.17.6 +node -v // should be 14.17.6 +``` + +### (Optional) Clean Install Modules +``` +yarn install --frozen-lockfile` +``` + +## Local development +- Go to your local branch +- `yarn` - To install packages that may have changed since your last branch +- `yarn build` - To do a clean build of js +- `yarn start` - To start the local server - navigate to [http://localhost:3000](http://localhost:3000) +## Dev workflow +We use [git flow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow#:~:text=The%20overall%20flow%20of%20Gitflow,branch%20is%20created%20from%20main&text=When%20a%20feature%20is%20complete%20it%20is%20merged%20into%20the,branch%20is%20created%20from%20main) + +### Summary +- Make sure master is always production ready +- Create feature/bug branches for every issue; put issue ID brance name, e.g. "bug/163-fix-step-count" for [Issue #163](https://github.com/irllabs/roundaround/issues/163) +- Merge `feature` branches into `stage` as soon as it's ready to minimize merge conflicts and lack of transparency in what the code will look like once it's deployed to `prod`. + +### Branchines +- `master` - always runs, is deployed to `prod` (http://rounds.studio and http:/rounds.irl.studio) +- `stage` - always runs, features are merged to it; is deployed to `stage` (http://roundaround-stage.web.app) when testing integration of features +- `feature/-` - one for each issue labeled as `enhancement` in github, deployed to `dev` (http://roundaround-dev.web.app) when testing a feature is useful +- `bug//-` - one for each issue labeled as `bug`, deployed to `dev` (http://roundaround-dev.web.app) when testing a bug is useful + +### How is new work added? +- checkout `stage` and create a new feature/bug branch +- If you want feedback deploy that branch out to `roundaround-dev.web.app`. It's fine if its buggy at the point of feedback +- When you are confident the new feature is completed make a PR and do a [full regression test](https://docs.google.com/spreadsheets/d/1fn3mY7sy1YfqoeCXUstYxEqKOidWj6KFN_negDrXKeQ/edit#gid=116044031). + + Deploy to `roundaround-dev.web.app`. Ask product to test the added functionality. They will _not_ do a regression +- If the tests pass, the PR is approved, and we're happy with the added functionality, merge that branch to stage. + + Deploy to `https://roundaround-stage.web.app/` +- When we want to merge into master, deploy that (or those) features to the stage server if they aren't already, and _everyone_ does a full regression before we make a PR for stage to master +- If the tests pass we merge to master. + + Deploy to `roundaround-dev.web.app` + +Summary - we never make a branch off master, only stage, and we only ever merge into master after stage is fully regression tested. We try to get feature branches into stage as soon as possible, so we can be confident we're always moving forward building on tested and verified work. + + + +## Testing +- As of now there's a git hook to make sure any code committed is linted and doesn't add malformed js +- As of now there's a smoke test that runs locally to make sure the site still loads when pushing + +If the smoke test fails you can debug it with: +`yarn run cypress:open` + +make sure the site is running locally. + ## Deploy frontend - `yarn build` - `firebase deploy --only hosting:production` + Should update `https://roundaround.web.app/`, this should always be master - `firebase deploy --only hosting:stage` - + Should update `https://roundaround-stage.web.app/`, this should always be develop and be in a stablish state +- `firebase deploy --only hosting:dev` + Should update `https://roundaround-dev.web.app/`, this can be any branch off develop, it's fine if it's buggy + ## Deploy functions (generates Jitsi tokens) - Make sure you have the jaasauth.pk private key file in the root of the functions folder (not kept in git) -- `firebase deploy --only functions` \ No newline at end of file +- `firebase deploy --only functions` diff --git a/cypress.json b/cypress.json new file mode 100644 index 0000000..e8c3fb4 --- /dev/null +++ b/cypress.json @@ -0,0 +1,12 @@ +{ + + "chromeWebSecurity": false, + "baseUrl": "https://roundaround-dev.web.app/", + "defaultCommandTimeout": 20000, + "viewportWidth": 1300, + "reporter": "junit", + "reporterOptions": { + "mochaFile": "cypress/results/cypress-report-[hash].xml" + }, + "projectId": "miag47" +} diff --git a/cypress/.eslintrc.json b/cypress/.eslintrc.json new file mode 100644 index 0000000..b677df5 --- /dev/null +++ b/cypress/.eslintrc.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "plugin:cypress/recommended" + ] +} \ No newline at end of file diff --git a/cypress/integration/smoke/see-a-round.spec.js b/cypress/integration/smoke/see-a-round.spec.js new file mode 100644 index 0000000..062f9bd --- /dev/null +++ b/cypress/integration/smoke/see-a-round.spec.js @@ -0,0 +1,60 @@ +const crypto = require('crypto'); + +describe("Can see a round", () => { + let hash; + + beforeEach(() => { + hash = crypto.randomBytes(3).toString('hex'); + cy.clearLocalStorage(); + cy.clearCookies(); + }); + + it("As a guest", () => { + cy.logout(); + + cy.get("[data-test=button-get-started]").click(); + cy.get("[data-test=app]").should("be.visible"); + + cy.get("[data-test=button-guest]").click(); + cy.get("[data-test=app]").should("be.visible"); + + cy.get("[data-test=input-name]").type(`test-${hash}`); + cy.get("[data-test=button-name]").click(); + + cy.get("[data-test=app]").should("be.visible"); + cy.get(".round").should("be.visible"); + + cy.logout(); + + cy.get("[data-test=app]").should("be.visible"); + }); + + it("As a registered user", () => { + cy.login(); + + cy.get('html').then(($html) => { + if ($html.find("[data-test=list-item-round]").length) { + cy.get("[data-test=list-item-round]").first().click(); + cy.get("[data-test=app]").should("be.visible"); + cy.get(".round").should("be.visible"); + cy.logout(); + cy.get("[data-test=app]").should("be.visible"); + } else if ($html.find("[data-test=button-new-round]").length) { + cy.get("[data-test=button-new-round]").click(); + cy.get("[data-test=app]").should("be.visible"); + cy.get(".round").should("be.visible"); + cy.logout(); + cy.get("[data-test=app]").should("be.visible"); + } + else { + cy.get("[data-test=button-back-to-rounds]").click(); + cy.get("[data-test=button-new-round]").should("be.visible"); + cy.get("[data-test=button-new-round]").click(); + cy.get("[data-test=app]").should("be.visible"); + cy.get(".round").should("be.visible"); + cy.logout(); + cy.get("[data-test=app]").should("be.visible"); + } + }) + }); +}); \ No newline at end of file diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 0000000..59b2bab --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,22 @@ +/// +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +/** + * @type {Cypress.PluginConfig} + */ +// eslint-disable-next-line no-unused-vars +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +} diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 0000000..33cbef3 --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,50 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) + +import { users } from "./users"; + +Cypress.Commands.add("logout", () => { + cy.visit("/"); + cy.get("[data-test=button-sign-in-out]").then(($btn) => { + if($btn.hasClass("signed-in")) { + cy.get("[data-test=button-sign-in-out]").click(); + cy.get("[data-test=button-sign-out]").click(); + cy.visit("/"); + } + }); +}); + +Cypress.Commands.add("login", () => { + cy.logout(); + + cy.get("[data-test=button-sign-in-out]").click(); + cy.get("[data-test=button-email]").click(); + cy.get("[data-test=input-email]").type(users.EMAIL_USER.username); + cy.get("[data-test=input-password]").type(users.EMAIL_USER.password); + cy.get("[data-test=button-sign-in]").click(); + cy.wait(1000); + cy.get("[data-test=button-get-started]").click(); +}); \ No newline at end of file diff --git a/cypress/support/index.js b/cypress/support/index.js new file mode 100644 index 0000000..d68db96 --- /dev/null +++ b/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/cypress/support/users.js b/cypress/support/users.js new file mode 100644 index 0000000..288d0e1 --- /dev/null +++ b/cypress/support/users.js @@ -0,0 +1,7 @@ +export const users = { + EMAIL_USER: { + "description": "default persistent test user", + "username": "roundabout-test@protonmail.com", + "password": "542cE_d974b5!3ae" + } +} \ No newline at end of file diff --git a/firebase.json b/firebase.json index 32b4036..fa95cca 100644 --- a/firebase.json +++ b/firebase.json @@ -1,47 +1,52 @@ { "hosting": [ { - "target":"dev", - "public": "build", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ], - "rewrites": [ - { - "source": "**", - "destination": "/index.html" - } - ] - },{ - "target":"stage", - "public": "build", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ], - "rewrites": [ - { - "source": "**", - "destination": "/index.html" - } - ] - }, - { - "target":"production", - "public": "build", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ], - "rewrites": [ - { - "source": "**", - "destination": "/index.html" - } - ] - }] + "target": "dev", + "public": "build", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + }, + { + "target": "stage", + "public": "build", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + }, + { + "target": "production", + "public": "build", + "ignore": [ + "firebase.json", + "**/.*", + "**/node_modules/**" + ], + "rewrites": [ + { + "source": "**", + "destination": "/index.html" + } + ] + } + ], + "functions": { + "source": "functions" + } } diff --git a/package.json b/package.json index 57fb400..54b8eba 100644 --- a/package.json +++ b/package.json @@ -1,60 +1,68 @@ { - "name": "roundaround", - "version": "0.1.0", - "private": true, - "dependencies": { - "@material-ui/core": "^4.11.3", - "@material-ui/icons": "^4.11.2", - "@svgdotjs/svg.js": "^3.0.16", - "@svgdotjs/svg.panzoom.js": "^2.1.1", - "@testing-library/jest-dom": "^5.11.4", - "@testing-library/react": "^11.1.0", - "@testing-library/user-event": "^12.1.10", - "@tonaljs/tonal": "^4.6.0", - "array-move": "^3.0.1", - "copy-webpack-plugin": "^7.0.0", - "deep-object-diff": "^1.1.0", - "firebase": "^8.2.5", - "immutability-helper": "^3.1.1", - "lodash": "^4.17.20", - "opus-media-recorder": "^0.8.0", - "qrcode": "^1.4.4", - "react": "^17.0.1", - "react-color": "^2.19.3", - "react-dom": "^17.0.1", - "react-dropzone": "^11.3.1", - "react-loader-spinner": "^4.0.0", - "react-redux": "^7.2.2", - "react-router-dom": "^5.2.0", - "react-scripts": "4.0.1", - "react-sortable-hoc": "^1.11.0", - "redux": "^4.0.5", - "sfz-parser": "^0.0.9", - "tone": "^14.7.77", - "web-vitals": "^0.2.4" - }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } + "name": "roundaround", + "version": "0.1.0", + "private": true, + "dependencies": { + "@material-ui/core": "^4.11.3", + "@material-ui/icons": "^4.11.2", + "@svgdotjs/svg.js": "^3.0.16", + "@svgdotjs/svg.panzoom.js": "^2.1.1", + "@testing-library/jest-dom": "^5.11.4", + "@testing-library/react": "^11.1.0", + "@testing-library/user-event": "^12.1.10", + "@tonaljs/tonal": "^4.6.0", + "array-move": "^3.0.1", + "copy-webpack-plugin": "^7.0.0", + "deep-object-diff": "^1.1.0", + "firebase": "^8.2.5", + "immutability-helper": "^3.1.1", + "lodash": "^4.17.20", + "opus-media-recorder": "^0.8.0", + "qrcode": "^1.4.4", + "react": "^17.0.1", + "react-color": "^2.19.3", + "react-dom": "^17.0.1", + "react-dropzone": "^11.3.1", + "react-loader-spinner": "^4.0.0", + "react-redux": "^7.2.2", + "react-router-dom": "^5.2.0", + "react-scripts": "4.0.1", + "react-sortable-hoc": "^1.11.0", + "redux": "^4.0.5", + "sfz-parser": "^0.0.9", + "tone": "^14.7.77", + "web-vitals": "^0.2.4" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test", + "eject": "react-scripts eject", + "prepare": "husky install", + "cypress:open": "CYPRESS_BASE_URL=http://localhost:3000/ cypress open", + "cypress:run": "CYPRESS_BASE_URL=http://localhost:3000/ cypress run" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "cypress": "^8.3.1", + "eslint-plugin-cypress": "^2.12.1", + "husky": "^7.0.0" + } } diff --git a/public/samples/HiHats/.DS_Store b/public/samples/HiHats/.DS_Store new file mode 100644 index 0000000..cd02caa Binary files /dev/null and b/public/samples/HiHats/.DS_Store differ diff --git a/src/App.css b/src/App.css index 405b3e2..31a3580 100644 --- a/src/App.css +++ b/src/App.css @@ -1,13 +1,13 @@ -html{ - height:100%; +html { + height: 100%; } -body{ - height:100%; - background-color:#1B1B1B !important; +body { + height: 100%; + background-color: #1b1b1b !important; } -#root{ - height:100%; +#root { + height: 100%; +} +.App { + height: 100%; } -.App{ - height:100%; -} \ No newline at end of file diff --git a/src/App.js b/src/App.js index 8f2abc9..8f5ec8e 100644 --- a/src/App.js +++ b/src/App.js @@ -19,7 +19,7 @@ import DeleteRoundDialog from './components/dialogs/DeleteRoundDialog'; -function App ({ setUser, setRounds, setIsShowingSignInDialog }) { +function App({ setUser, setRounds, setIsShowingSignInDialog }) { const theme = React.useMemo( () => unstable_createMuiStrictModeTheme({ @@ -42,6 +42,15 @@ function App ({ setUser, setRounds, setIsShowingSignInDialog }) { active: '#EAEAEA' } }, + breakpoints: { + values: { + xs: 0, + sm: 500, + md: 900, + lg: 1200, + xl: 1536, + }, + }, shape: { borderRadius: 32 }, @@ -57,7 +66,7 @@ function App ({ setUser, setRounds, setIsShowingSignInDialog }) { return ( -
+
diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index 1f03afe..0000000 --- a/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/src/audio-engine/AudioEngine.js b/src/audio-engine/AudioEngine.js index 78d6f0c..50099a7 100644 --- a/src/audio-engine/AudioEngine.js +++ b/src/audio-engine/AudioEngine.js @@ -1,6 +1,7 @@ import * as Tone from 'tone'; import Track from './Track'; -import _ from 'lodash' +import _ from 'lodash'; +import { arraymove } from '../utils'; const AudioEngine = { tracks: [], @@ -8,7 +9,7 @@ const AudioEngine = { tracksByType: {}, busesByUser: {}, master: null, - init () { + init() { const _this = this return new Promise(async (resolve, reject) => { _this.master = new Track({ @@ -18,8 +19,8 @@ const AudioEngine = { resolve() }) }, - async load (round) { - // console.log('audio engine loading round', round); + async load(round) { + console.log('audio engine loading round', round); const _this = this return new Promise(async (resolve, reject) => { _this.round = round @@ -29,17 +30,24 @@ const AudioEngine = { _this.setSwing(round.swing) } for (const userBus of Object.values(round.userBuses)) { + //check if lowpass and highpass are positioned second and third then move them to fourth and fifth positions + if (userBus.fx[1].name === 'lowpass' && userBus.fx[2].name === 'highpass') { + await arraymove(userBus.fx, 1, 4); + await arraymove(userBus.fx, 1, 4); + } await _this.addUser(userBus.id, userBus.fx) } for (const layer of round.layers) { + console.log('creating track', layer.id) const track = await _this.createTrack(layer) + console.log('created track', track) await track.load(layer, round.userPatterns[layer.createdBy]) }; // console.log('audio engine finsihed loading round'); resolve() }) }, - async addUser (userId, userFx) { + async addUser(userId, userFx) { return new Promise(async (resolve, reject) => { //console.log('addUser()', userId); const userBus = await this.createTrack({ fx: userFx, id: userId, createdBy: userId, type: Track.TRACK_TYPE_USER }) @@ -48,25 +56,25 @@ const AudioEngine = { resolve() }) }, - play () { + play() { this.startAudioContext() - Tone.Transport.start("+0.1"); + Tone.Transport.start("+0.1") Tone.Transport.loop = false Tone.Transport.loopEnd = '1:0:0' }, - stop () { + stop() { Tone.Transport.stop() }, - startAudioContext () { + startAudioContext() { if (Tone.context.state !== 'running') { Tone.context.resume(); } }, - isOn () { + isOn() { return Tone.Transport.state === 'started' }, // assumes tracks haven't changed, just the steps - recalculateParts (round, layerId = null) { + recalculateParts(round, layerId = null) { console.log('AudioEngine::recalculateParts()'); console.time('AudioEngine::recalculateParts') if (!_.isNil(round)) { @@ -81,18 +89,19 @@ const AudioEngine = { } console.timeEnd('AudioEngine::recalculateParts') }, - getIsPlayingSequence (userId, round) { + getIsPlayingSequence(userId, round) { return round.userPatterns[userId].isPlayingSequence }, - createTrack (trackParameters) { + createTrack(trackParameters) { const userId = trackParameters.createdBy const type = trackParameters.type // console.log('createTrack', trackParameters, userId, type); let _this = this return new Promise(async function (resolve, reject) { + console.log('pre track --') let track = new Track(trackParameters, type, userId) - + console.log('track --', track) _this.tracks.push(track) _this.tracksById[track.id] = track if (_.isNil(_this.tracksByType[track.type])) { @@ -108,12 +117,12 @@ const AudioEngine = { resolve(track) }) }, - removeTrack (id) { + removeTrack(id) { if (!_.isNil(this.tracksById[id])) { this.tracksById[id].dispose() } }, - reset () { + reset() { for (let track of this.tracks) { track.dispose() } @@ -122,20 +131,20 @@ const AudioEngine = { this.tracksByType = {} }, - releaseAll () { + releaseAll() { for (let track of this.tracks) { track.releaseAll() } }, - getPositionMilliseconds () { + getPositionMilliseconds() { return Math.round(Tone.Transport.seconds * 1000) }, - setTempo (bpm) { + setTempo(bpm) { Tone.Transport.bpm.value = bpm // need to recalculate parts because absolute time offset needs to be recalculated this.recalculateParts(this.round) }, - setSwing (swing) { + setSwing(swing) { Tone.Transport.swing = swing / 100 } diff --git a/src/audio-engine/CustomSamples.js b/src/audio-engine/CustomSamples.js index 6686ac8..b906844 100644 --- a/src/audio-engine/CustomSamples.js +++ b/src/audio-engine/CustomSamples.js @@ -3,17 +3,17 @@ import _ from "lodash"; const CustomSamples = { samples: {}, - init (firebase) { + init(firebase) { this.firebase = firebase }, - add (sample) { + add(sample) { if (!_.isNil(sample)) { // console.log('CustomSamples:add() sample', sample); this.samples[sample.id] = sample // console.log('CustomSamples::added', this.samples); } }, - async get (id) { + async get(id) { // console.log('CustomSamples::get()', id); const _this = this return new Promise(async (resolve, reject) => { @@ -23,7 +23,7 @@ const CustomSamples = { if (!_.isNil(id)) { try { let sample = await this.firebase.getSample(id) - // console.log('CustomSamples::get() from firebase', sample); + //console.log('CustomSamples::get() from firebase', sample); _this.samples[id] = _.cloneDeep(sample) resolve(sample) } catch (e) { @@ -34,7 +34,7 @@ const CustomSamples = { resolve(null) }) }, - delete (sampleId, userId) { + delete(sampleId, userId) { return new Promise(async (resolve, reject) => { await this.firebase.deleteSample(sampleId) const fileRef = this.firebase.storage.ref().child(userId + '/' + sampleId + '.wav') @@ -43,7 +43,7 @@ const CustomSamples = { resolve() }) }, - rename (sampleId, newName) { + rename(sampleId, newName) { // console.log('CustomSample::rename', sampleId, newName); this.samples[sampleId].name = newName this.firebase.updateSample({ id: sampleId, name: newName }) diff --git a/src/audio-engine/FX.js b/src/audio-engine/FX.js index 92b6eca..b9df1ee 100644 --- a/src/audio-engine/FX.js +++ b/src/audio-engine/FX.js @@ -1,5 +1,6 @@ import Delay from '../audio-engine/fx/delay'; import Lowpass from '../audio-engine/fx/lowpass'; +import PingPong from '../audio-engine/fx/pingpong'; import Highpass from '../audio-engine/fx/highpass'; import Distortion from '../audio-engine/fx/distortion'; import Bitcrusher from '../audio-engine/fx/bitcrusher'; @@ -11,13 +12,13 @@ const FX = { fxClasses: {}, fx: [], fxById: {}, - init () { - let classes = [Lowpass, Delay, Highpass, Distortion, Bitcrusher, Autowah, Reverb] + init() { + let classes = [Lowpass, Delay, PingPong, Highpass, Distortion, Bitcrusher, Autowah, Reverb] for (let fxClass of classes) { this.fxClasses[fxClass.fxName] = fxClass } }, - create (fxParameters) { + create(fxParameters) { let _this = this return new Promise(async function (resolve, reject) { let fxClass = _this.fxClasses[fxParameters.name] @@ -27,16 +28,16 @@ const FX = { resolve(fx) }) }, - reset () { + reset() { // todo }, - dispose () { + dispose() { // todo }, - getUI (name) { + getUI(name) { return this.fxClasses[name].ui }, - getIcon (name) { + getIcon(name) { let fxClass = this.fxClasses[name] if (!_.isNil(fxClass)) { return this.fxClasses[name].icon diff --git a/src/audio-engine/Instruments.js b/src/audio-engine/Instruments.js index 21917a0..f84d86e 100644 --- a/src/audio-engine/Instruments.js +++ b/src/audio-engine/Instruments.js @@ -4,39 +4,76 @@ import HiHats from './instruments/HiHats' import Kicks from './instruments/Kicks' import Snares from './instruments/Snares' import Perc from './instruments/Perc' -import Metal from './instruments/Metal' import Custom from './instruments/Custom' import CustomSamples from './CustomSamples' +import { randomInt } from "../utils"; const Instruments = { instrumentClasses: {}, instruments: [], - init () { + async init() { const classes = [ HiHats, Kicks, Snares, Perc, - Metal, Custom ]; for (let instrumentClass of classes) { this.instrumentClasses[instrumentClass.instrumentName] = instrumentClass; } }, - create (instrumentName, articulation) { - if (!_.isNil(instrumentName)) { - let _this = this; - return new Promise(async function (resolve, reject) { - let InstrumentClass = _this.instrumentClasses[instrumentName]; - let instrument = new InstrumentClass(); - await instrument.load(articulation); - _this.instruments.push(instrument); - resolve(instrument); - }); + async getRandomArticulation(instrumentName) { + const instruments = await this.classes() + let randomSoundNo = 0 + const instrument = instruments[instrumentName] + const sampleKeys = instrument['sampleKeys'] + randomSoundNo = await randomInt(0, sampleKeys.length - 1) + return sampleKeys[randomSoundNo] + }, + async classes() { + const classes = [ + HiHats, + Kicks, + Snares, + Perc, + Custom + ]; + const inst = {}; + for (let instrument of classes) { + inst[instrument.instrumentName] = { + instrumentName: instrument.instrumentName, + name: instrument.name, + label: instrument.label, + samples: instrument.articulations, + sampleKeys: Object.keys(instrument.articulations) + }; } + return inst; }, - dispose (id) { + + async create(instrumentName, articulation, articulationId) { + let _this = this + return new Promise(async (resolve, reject) => { + if (!_this.instrumentClasses[instrumentName]) + await this.init() + if (!_.isNil(instrumentName)) { + let InstrumentClass = _this.instrumentClasses[instrumentName] + if (InstrumentClass) { + let instrument = new InstrumentClass() + if (instrumentName === 'custom' && articulationId) { + await instrument.load(articulationId) + } + else + await instrument.load(articulation) + _this.instruments = [..._this.instruments, instrument] + resolve(instrument) + } + } + else reject(null) + }); + }, + dispose(id) { let instrument = _.find(this.instruments, { id }); @@ -44,12 +81,12 @@ const Instruments = { instrument.dispose(); } }, - updateParameter (instrumentId, parameter, value) { + updateParameter(instrumentId, parameter, value) { _.find(this.instruments, { id: instrumentId }).updateParameter(parameter, value); }, - getInstrumentOptions (includeCustom = true) { + getInstrumentOptions(includeCustom = true) { let options = []; for (let [, instrument] of Object.entries(this.instrumentClasses)) { if (instrument.instrumentName !== 'custom' || includeCustom) { @@ -63,8 +100,7 @@ const Instruments = { options = _.sortBy(options, "label"); return options; }, - getInstrumentArticulationOptions (instrumentName, userId) { - // console.log('getInstrumentArticulationOptions()', instrumentName); + getInstrumentArticulationOptions(instrumentName, userId, instrument) { if (instrumentName !== 'custom') { let options = []; for (let [, value] of Object.entries( @@ -76,34 +112,39 @@ const Instruments = { }; options.push(option); } - // console.log('got instrument options', options); return options; } else { let options = [] - // console.log('CustomSamples.samples', CustomSamples.samples); - for (let [id, sample] of Object.entries(CustomSamples.samples)) { - if (sample.createdBy === userId) { - options.push({ - name: sample.name, - value: id - }) + if (!instrument.sampleId) + for (let [id, sample] of Object.entries(CustomSamples.samples)) { + if (sample.createdBy === userId) { + options.push({ + name: sample.name, + value: id + }) + } } + else { + options.push({ + name: instrument.displayName, + value: instrument.sample + }) } - // console.log('got sample options', options); return options } }, - getLabel (instrumentName) { + getLabel(instrumentName) { return this.instrumentClasses[instrumentName].label; }, - getArticulationLabel (instrumentName, articulation) { + getArticulationLabel(instrumentName, articulation) { return this.instrumentClasses[instrumentName].articulations[articulation]; }, - getInstrumentLabel (instrumentName) { - return this.instrumentClasses[instrumentName].label + getInstrumentLabel(instrumentName) { + let label = '' + if (instrumentName && this.instrumentClasses[instrumentName]) label = this.instrumentClasses[instrumentName].label + return label }, - getDefaultArticulation (instrumentName) { - //console.log('Instruments::getDefaultArticulation() instrumentName', instrumentName, this.instrumentClasses); + getDefaultArticulation(instrumentName) { return this.instrumentClasses[instrumentName].defaultArticulation /*return !_.isNil(this.instrumentClasses[instrumentName].defaultArticulation) ? this.instrumentClasses[instrumentName].defaultArticulation diff --git a/src/audio-engine/Track.js b/src/audio-engine/Track.js index 2a5619a..4c19c56 100644 --- a/src/audio-engine/Track.js +++ b/src/audio-engine/Track.js @@ -11,7 +11,7 @@ export default class Track { static TRACK_TYPE_USER = 'TRACK_TYPE_USER' // User busses are routed to master static TRACK_TYPE_MASTER = 'TRACK_TYPE_MASTER' static TRACK_TYPE_AUTOMATION = 'TRACK_TYPE_AUTOMATION' // Each layer is routed to a user bus - constructor (trackParameters, type, userId) { + constructor(trackParameters, type, userId) { this.trackParameters = trackParameters this.id = trackParameters.id this.userId = userId @@ -19,9 +19,10 @@ export default class Track { this.instrument = null this.automation = null this.notes = null + console.log('setting type --') this.setType(type) } - setType (type, automationFxId) { + setType(type, automationFxId) { this.dispose() this.type = type; this.trackParameters.type = type @@ -53,9 +54,11 @@ export default class Track { } this.automation = new Automation(this.trackParameters.automationFxId, this.userId) } + console.log('pre parts calc') + console.log('user patterns', this.userPatterns) this.calculatePart(this.trackParameters, this.userPatterns) } - setFxOrder (updatedFxOrders) { + setFxOrder(updatedFxOrders) { /* this.disconnectAudioChain() for (let updatedFxOrder of updatedFxOrders) { _.find(this.sortedFx, { id: updatedFxOrder.id }).order = updatedFxOrder.order @@ -63,13 +66,13 @@ export default class Track { this.sortedFx = _.sortBy(this.sortedFx, 'order') this.buildAudioChain()*/ } - load (trackParameters, userPatterns) { + load(trackParameters, userPatterns) { this.trackParameters = trackParameters this.userPatterns = userPatterns this.calculatePart(trackParameters, userPatterns) } - async createFX (fxList) { + async createFX(fxList) { return new Promise(async (resolve, reject) => { if (!_.isNil(fxList)) { this.fx = {} @@ -84,7 +87,7 @@ export default class Track { resolve() }) } - buildAudioChain () { + buildAudioChain() { // console.log('Track::buildAudioChain()', this.type, this.id, this.instrument); if (this.type === Track.TRACK_TYPE_MASTER) { this.channel.toDestination() @@ -145,7 +148,7 @@ export default class Track { } } - disconnectAudioChain () { + disconnectAudioChain() { if (!_.isNil(this.channel) && !_.isNil(this.channel.context._context)) { this.channel.disconnect(0) } @@ -159,7 +162,7 @@ export default class Track { } } - dispose () { + dispose() { this.disconnectAudioChain() if (!_.isNil(this.instrument)) { this.instrument.dispose() @@ -180,8 +183,8 @@ export default class Track { } } } - calculatePart (layer, userPatterns) { - //console.log('Track::calculatePart()', layer, userPatterns); + calculatePart(layer, userPatterns) { + console.log('Track::calculatePart()', layer, userPatterns); this.trackParameters = layer if (!_.isNil(userPatterns)) { // if (!userPatterns.isPlayingSequence) { @@ -204,7 +207,7 @@ export default class Track { }*/ } } - convertStepsToNotes (steps, percentOffset, timeOffset) { + convertStepsToNotes(steps, percentOffset, timeOffset) { const PPQ = Tone.Transport.PPQ const totalTicks = PPQ * 4 const ticksPerStep = Math.round(totalTicks / steps.length) @@ -240,17 +243,18 @@ export default class Track { // console.log('notes', notes); return notes } - msToTicks (ms) { + msToTicks(ms) { const BPM = Tone.Transport.bpm.value const PPQ = Tone.Transport.PPQ const msPerBeat = 60000 / BPM const msPerTick = msPerBeat / PPQ return Math.round(ms / msPerTick) } - async setInstrument (instrument) { + async setInstrument(instrument) { // console.time('setInstrument', instrument) const instrumentName = instrument.sampler const articulation = instrument.sample + const articulationId = instrument.sampleId let _this = this return new Promise(async function (resolve, reject) { if (!_.isNil(_this.instrument)) { @@ -262,7 +266,8 @@ export default class Track { } _this.instrument = await Instruments.create( instrumentName, - articulation + articulation, + articulationId ) // console.log('instrument created') _this.buildAudioChain() @@ -275,10 +280,10 @@ export default class Track { resolve(_this.instrument) }) } - clearInstrument () { + clearInstrument() { Instruments.dispose(this.instrument.id) } - setAutomatedFx (fxId) { + setAutomatedFx(fxId) { if (!_.isNil(this.automation)) { this.automation.setFx(fxId) } else { @@ -286,10 +291,10 @@ export default class Track { } this.calculatePart(this.trackParameters, this.userPatterns) } - createAutomation (fxId, userId) { + createAutomation(fxId, userId) { this.automation = new Automation(fxId, userId) } - setVolume (value) { + setVolume(value) { //console.log('Track::setVolume()', value); const _this = this // temporary hack, todo investigate why this is necessary (when loading a preset the volume sometimes doesn't update) @@ -297,10 +302,10 @@ export default class Track { _this.channel.volume.value = value }, 300) } - setSolo (value) { + setSolo(value) { this.channel.solo = value } - setMute (value) { + setMute(value) { // console.log('Track::setMute()', value); const _this = this // temporary hack, todo investigate why this is necessary (when loading a preset the mute sometimes doesn't work) @@ -308,7 +313,7 @@ export default class Track { _this.channel.mute = value }, 300) } - async setMixerSettings (settings) { + async setMixerSettings(settings) { let _this = this return new Promise(async function (resolve, reject) { await _this.setStyle(settings.style) @@ -321,32 +326,32 @@ export default class Track { resolve() }) } - async setFXIsOn (fxId, value) { + async setFXIsOn(fxId, value) { // console.log('setFXIsOn', fxId, value); this.disconnectAudioChain() this.fx[fxId].isOn = value this.buildAudioChain() } - setFXParameter (fxId, parameter, value) { + setFXParameter(fxId, parameter, value) { if (this.fx[fxId].isOn) { this.fx[fxId][parameter] = value } } - releaseAll () { + releaseAll() { if (!_.isNil(this.instrument)) { this.instrument.releaseAll() } } - triggerNote (note) { + triggerNote(note) { this.instrument.triggerNote(note) } - triggerAttack (pitch, velocity) { + triggerAttack(pitch, velocity) { this.instrument.triggerAttack(pitch, velocity) } - triggerRelease (pitch) { + triggerRelease(pitch) { this.instrument.triggerRelease(pitch) } - getNotes () { + getNotes() { return this.notes } } diff --git a/src/audio-engine/fx/autowah.js b/src/audio-engine/fx/autowah.js index b2246aa..59c108f 100644 --- a/src/audio-engine/fx/autowah.js +++ b/src/audio-engine/fx/autowah.js @@ -4,9 +4,14 @@ import * as Tone from 'tone'; export default class Autowah extends FXBaseClass { static fxName = 'autowah'; - static icon = '< g clip - path="url(#clip0)" >' + static icon = + ` + + + + ` - constructor (fxParameters) { + constructor(fxParameters) { super(fxParameters) this._q = 4 this._mix = 1 @@ -15,13 +20,13 @@ export default class Autowah extends FXBaseClass { this.isOn = fxParameters.isOn } - setQ (value, time) { + setQ(value, time) { this._q = value if (this.isOn) { this.fx.Q = value } } - setMix (value, time) { + setMix(value, time) { this._mix = value if (this.isOn) { if (!_.isNil(time)) { @@ -31,7 +36,7 @@ export default class Autowah extends FXBaseClass { } } } - setBypass (value, time) { + setBypass(value, time) { if (value === true && !this._override) { // set mix to 0 rather than turn off so that we can do this rapidly without needing to rebuild the audio chain if (this._mix > 0) { @@ -44,13 +49,13 @@ export default class Autowah extends FXBaseClass { } - enable () { + enable() { this.fx = new Tone.AutoWah(50, 6, -30) this.fx.wet.value = this._mix this.setBypass(true) } - getAutomationOptions () { + getAutomationOptions() { return [ { label: 'Enabled', diff --git a/src/audio-engine/fx/delay.js b/src/audio-engine/fx/delay.js index 0fe5d9b..5f00aa0 100644 --- a/src/audio-engine/fx/delay.js +++ b/src/audio-engine/fx/delay.js @@ -5,32 +5,35 @@ import * as Tone from 'tone'; export default class Delay extends FXBaseClass { static fxName = 'delay'; - // static icon = '' - static icon = '' + static icon = + ` + + + ` - constructor (fxParameters) { + constructor(fxParameters) { super(fxParameters) this._mix = 0.2 this._mixBeforeBypass = this._mix this.label = 'Tape delay' - this._delayTime = '8t'; + this._delayTime = '3t'; this._feedback = 0.7 this.isOn = fxParameters.isOn } - setDelayTime (value) { + setDelayTime(value) { this._delayTime = value if (this.isOn) { this.fx.delayTime.value = value } } - setFeedback (value) { + setFeedback(value) { this._feedback = value if (this.isOn) { this.fx.feedback.value = value } } - setMix (value, time) { + setMix(value, time) { this._mix = value if (this.isOn) { if (!_.isNil(time)) { @@ -40,7 +43,7 @@ export default class Delay extends FXBaseClass { } } } - setBypass (value, time) { + setBypass(value, time) { if (value === true && !this._override) { // set mix to 0 rather than turn off so that we can do this rapidly without needing to rebuild the audio chain if (this._mix > 0) { @@ -52,13 +55,13 @@ export default class Delay extends FXBaseClass { } } - enable () { + enable() { this.fx = new Tone.FeedbackDelay(this._delayTime, this._feedback) this.fx.wet.value = this._mix this.setBypass(true) } - getAutomationOptions () { + getAutomationOptions() { return [ { label: 'Enabled', diff --git a/src/audio-engine/fx/distortion.js b/src/audio-engine/fx/distortion.js index 13a0a56..638effa 100644 --- a/src/audio-engine/fx/distortion.js +++ b/src/audio-engine/fx/distortion.js @@ -5,9 +5,14 @@ import * as Tone from 'tone'; export default class Distortion extends FXBaseClass { static fxName = 'distortion'; - static icon = '' + static icon = + ` + + + + ` - constructor (fxParameters) { + constructor(fxParameters) { super(fxParameters) this._amount = 4 this._mix = 0.2 @@ -16,13 +21,13 @@ export default class Distortion extends FXBaseClass { this.isOn = fxParameters.isOn } - setAmount (value, time) { + setAmount(value, time) { this._amount = value if (this.isOn) { this.fx.distortion = value } } - setMix (value, time) { + setMix(value, time) { this._mix = value if (this.isOn) { if (!_.isNil(time)) { @@ -32,7 +37,7 @@ export default class Distortion extends FXBaseClass { } } } - setBypass (value, time) { + setBypass(value, time) { if (value === true && !this._override) { // set mix to 0 rather than turn off so that we can do this rapidly without needing to rebuild the audio chain if (this._mix > 0) { @@ -45,13 +50,13 @@ export default class Distortion extends FXBaseClass { } - enable () { + enable() { this.fx = new Tone.Distortion(this._amount) this.fx.wet.value = this._mix this.setBypass(true) } - getAutomationOptions () { + getAutomationOptions() { return [ { label: 'Enabled', diff --git a/src/audio-engine/fx/highpass.js b/src/audio-engine/fx/highpass.js index e95c1aa..b042ae1 100644 --- a/src/audio-engine/fx/highpass.js +++ b/src/audio-engine/fx/highpass.js @@ -5,9 +5,13 @@ import * as Tone from 'tone'; export default class Highpass extends FXBaseClass { static fxName = 'highpass'; - static icon = '' + static icon = + ` + + + ` - constructor (fxParameters) { + constructor(fxParameters) { super(fxParameters) this._frequency = 4000 this._frequencyBeforeBypass = this._frequency @@ -16,13 +20,13 @@ export default class Highpass extends FXBaseClass { this.isOn = fxParameters.isOn } - set type (value) { + set type(value) { this._type = value if (this.isOn) { this.fx.type = value } } - setFrequency (value, time) { + setFrequency(value, time) { this._frequency = value if (this.isOn) { if (!_.isNil(time)) { @@ -32,7 +36,7 @@ export default class Highpass extends FXBaseClass { } } } - setBypass (value, time) { + setBypass(value, time) { if (value === true && !this._override) { // set frequency to min rather than turn off so that we can do this rapidly without needing to rebuild the audio chain if (this._frequency > 0) { @@ -44,12 +48,12 @@ export default class Highpass extends FXBaseClass { } } - enable () { + enable() { this.fx = new Tone.Filter(this._frequency, this._type) this.setBypass(true) } - getAutomationOptions () { + getAutomationOptions() { return [ { label: 'Enabled', diff --git a/src/audio-engine/fx/lowpass.js b/src/audio-engine/fx/lowpass.js index 7c95913..64a4d51 100644 --- a/src/audio-engine/fx/lowpass.js +++ b/src/audio-engine/fx/lowpass.js @@ -5,9 +5,13 @@ import * as Tone from 'tone'; export default class Lowpass extends FXBaseClass { static fxName = 'lowpass'; - static icon = '' + static icon = + ` + + + ` - constructor (fxParameters) { + constructor(fxParameters) { super(fxParameters) this._frequency = 500 this._frequencyBeforeBypass = this._frequency @@ -16,13 +20,13 @@ export default class Lowpass extends FXBaseClass { this.isOn = fxParameters.isOn } - set type (value) { + set type(value) { this._type = value if (this.isOn) { this.fx.type = value } } - setFrequency (value, time) { + setFrequency(value, time) { this._frequency = value if (this.isOn) { if (!_.isNil(time)) { @@ -32,7 +36,7 @@ export default class Lowpass extends FXBaseClass { } } } - setBypass (value, time) { + setBypass(value, time) { if (value === true && !this._override) { // set frequency to max rather than turn off so that we can do this rapidly without needing to rebuild the audio chain if (this._frequency < 20000) { @@ -44,12 +48,12 @@ export default class Lowpass extends FXBaseClass { } } - enable () { + enable() { this.fx = new Tone.Filter(this._frequency, this._type) this.setBypass(true) } - getAutomationOptions () { + getAutomationOptions() { return [ { label: 'Enabled', diff --git a/src/audio-engine/fx/pingpong.js b/src/audio-engine/fx/pingpong.js new file mode 100644 index 0000000..0e8e615 --- /dev/null +++ b/src/audio-engine/fx/pingpong.js @@ -0,0 +1,101 @@ + +import FXBaseClass from './fx-base-class'; +import _ from 'lodash'; +import * as Tone from 'tone'; + +export default class PingPong extends FXBaseClass { + static fxName = 'pingpong'; + static icon = + ` + + + + ` + + constructor(fxParameters) { + super(fxParameters) + this._mix = 0.2 + this._mixBeforeBypass = this._mix + this.label = 'Ping-pong delay' + this._delayTime = '9t'; + this._feedback = 0.7 + this.isOn = fxParameters.isOn + } + + setDelayTime(value) { + this._delayTime = value + if (this.isOn) { + this.fx.delayTime.value = value + } + } + setFeedback(value) { + this._feedback = value + if (this.isOn) { + this.fx.feedback.value = value + } + } + setMix(value, time) { + this._mix = value + if (this.isOn) { + if (!_.isNil(time)) { + this.fx.wet.setValueAtTime(value, time) + } else { + this.fx.wet.value = value + } + } + } + setBypass(value, time) { + if (value === true && !this._override) { + // set mix to 0 rather than turn off so that we can do this rapidly without needing to rebuild the audio chain + if (this._mix > 0) { + this._mixBeforeBypass = this._mix + } + this.setMix(0, time) + } else { + this.setMix(this._mixBeforeBypass, time) + } + } + + enable() { + this.fx = new Tone.PingPongDelay(this._delayTime, this._feedback) + this.fx.wet.value = this._mix + this.setBypass(true) + } + + getAutomationOptions() { + return [ + { + label: 'Enabled', + name: 'enabled', + setParameter: this.setBypass.bind(this), + calculateValue: function (value) { + return value === true ? false : true + } + }, + /*{ + label: 'Time', + value: 'delayTime', + calculateValue: function (value) { + // function to take a 0 - 1 value from interface and return appropriate value for this FX parameter + return numberRange(value, 0, 1, 0, 2000) + } + }, + { + label: 'Feedback', + value: 'feedback', + calculateValue: function (value) { + // function to take a 0 - 1 value from interface and return appropriate value for this FX parameter + return value + } + }, + { + label: 'Mix', + value: 'mix', + calculateValue: function (value) { + // function to take a 0 - 1 value from interface and return appropriate value for this FX parameter + return value + } + }*/ + ] + } +} diff --git a/src/audio-engine/instruments/Custom.js b/src/audio-engine/instruments/Custom.js index ab2e427..945dfee 100644 --- a/src/audio-engine/instruments/Custom.js +++ b/src/audio-engine/instruments/Custom.js @@ -1,18 +1,41 @@ -import InstrumentBaseClass from './InstrumentBaseClass'; +import * as Tone from 'tone' +import InstrumentBaseClass from './InstrumentBaseClass' +import { randomBool } from '../../utils/index' import _ from 'lodash' import CustomSamples from '../CustomSamples' export default class Custom extends InstrumentBaseClass { - static instrumentName = 'custom'; - static label = 'Custom'; - static folder = ''; + static instrumentName = 'custom' + static label = 'Custom' + static folder = '' static articulations = {} - static defaultArticulation = null; - constructor () { + static defaultArticulation = null + constructor(folder) { super(Custom.instrumentName, Custom.articulations, Custom.folder) + this.parameters = {} this.parameters.articulation = Custom.defaultArticulation; + this.name = 'custom' + this.articulations = {} + this.folder = folder + this.instrument = null + this.part = null + this.connectedToChannel = null } - getSampleMap (sample) { + updateParameter(parameter, value) { + this.parameters[parameter] = value + } + updateParameters(parameters) { + Object.keys(parameters).forEach((key) => { + if (key !== 'id') { + this.updateParameter(key, parameters[key]) + } + }) + } + connect(channel) { + this.connectedToChannel = channel + this.instrument.connect(this.connectedToChannel) + } + getSampleMap(sample) { let url = sample.localURL if (_.isNil(url)) { url = sample.remoteURL @@ -22,22 +45,99 @@ export default class Custom extends InstrumentBaseClass { } return map } - load (sampleId) { - // console.log('custom::load()', sampleId); - const _this = this - return new Promise(async function (resolve, reject) { + loaded() { + // to be overidden + } + load(sampleId) { + // console.log('custom::load()', sampleId); + return new Promise(async (resolve, reject) => { let sample = await CustomSamples.get(sampleId) - let sampleMap = _this.getSampleMap(sample) - _this.sampleMap = _.cloneDeep(sampleMap) - // console.log('instrument load()', sampleMap) + let sampleMap = this.getSampleMap(sample) + this.sampleMap = _.cloneDeep(sampleMap) if (!_.isNil(sampleMap)) { - await _this.loadSamples(sampleMap) + await this.loadSamples(sampleMap) } - // console.log('instrument finished loading'); resolve() }) } - calculateMidiNoteFromVelocity (velocity) { + loadSamples(sampleMap) { + return new Promise((resolve, reject) => { + this.dispose() + try { + this.instrument = new Tone.Sampler(sampleMap, { + onload: () => { + this.updateParameters(this.parameters) + if (!_.isNil(this.connectedToChannel)) { + this.instrument.connect(this.connectedToChannel) + } + this.loaded() + resolve() + } + }) + resolve() + } catch (e) { + console.log('error loading samples', e); + } + }) + } + loadPart( + notes, numberOfBars + ) { + let _this = this + // console.log('instrument loading notes', notes); + this.clearPart() + this.notes = _.cloneDeep(notes) + this.beforeLoadPart(this.notes) + for (let note of this.notes) { + note.time += 'i'; + note.duration += 'i'; + note.midi = this.calculateMidiNoteFromVelocity(note.velocity) + } + this.part = new Tone.Part(function (time, note) { + if ( + !_.isNil(_this.instrument) && + !_.isNil(_this.instrument.context) + ) { + let shouldPlayNote = true + if (note.probability < 1) { + shouldPlayNote = randomBool(note.probability) + } + if (shouldPlayNote) { + _this.instrument.triggerAttackRelease( + Tone.Midi(note.midi), + note.duration, + time, + note.velocity + ) + } + } + }, this.notes) + this.afterPartLoaded() + // console.log('this.part', this.part); + this.part.loop = true + this.part.loopEnd = numberOfBars + ":0:0" + this.part.start(0) + } + beforeLoadPart(notes) { + // to be overidden if necessary + } + afterPartLoaded() { + // to be overidden if necessary + } + dispose() { + if (!_.isNil(this.instrument) && !_.isNil(this.instrument._context)) { + this.instrument.releaseAll() + this.instrument.dispose() + } + this.instrument = null + this.clearPart() + } + clearPart() { + if (!_.isNil(this.part) && !_.isNil(this.part._events)) { + this.part.dispose() + } + } + calculateMidiNoteFromVelocity(velocity) { return 60 } } diff --git a/src/components/dialogs/CreateRoundDialog.js b/src/components/dialogs/CreateRoundDialog.js new file mode 100644 index 0000000..bd16a4b --- /dev/null +++ b/src/components/dialogs/CreateRoundDialog.js @@ -0,0 +1,483 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import React, { useState, useRef, useEffect, useContext } from 'react' +import { connect } from 'react-redux' +import { FirebaseContext } from '../../firebase'; + +import { IconButton, Button, Typography } from '@material-ui/core' +import Close from '../play/layer-settings/resources/Close' +import DialogTitle from '@material-ui/core/DialogTitle' +import Dialog from '@material-ui/core/Dialog' + +import { + setRedirectAfterSignIn, + setRounds, + setIsShowingDeleteRoundDialog, + setIsShowingRenameDialog, + setSelectedRoundId +} from '../../redux/actions' + +import { getDefaultSample } from '../../utils/defaultData' + + +import { Box, makeStyles } from '@material-ui/core' +import Di from '../play/layer-settings/resources/Di' +import Upload from '../play/layer-settings/resources/Upload' +import Loader from 'react-loader-spinner' +import Add from '../play/layer-settings/resources/Add' +import Playback from '../play/layer-settings/resources/Playback' +import Trash from '../play/layer-settings/resources/Trash' +import { cloneDeep } from 'lodash' + +const styles = makeStyles({ + paper: { + display: 'flex', + flexDirection: 'column', + borderRadius: 8, + width: 363 + }, + title: { + position: 'relative', + textAlign: 'center', + fontSize: 20 + }, + titleSub: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between' + }, + body: { + display: 'flex', + flexDirection: 'column', + height: 283, + padding: '1rem', + paddingBottom: 0, + overflow: 'hidden', + borderTop: 'solid 1px rgba(255,255,255,0.1)' + }, + close: { + width: 17.78, + height: 17.78, + cursor: 'pointer' + }, + closeContainer: { + position: 'absolute', + bottom: 5, + flex: 1, + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + cursor: 'pointer' + }, + titleText: { + flex: 1, + display: 'flex', + justifyContent: 'center', + textAlign: 'center', + marginLeft: 5 + }, + tileAlt: { + display: 'flex', + flexDirection: 'row', + width: 331, + height: 48, + borderRadius: 12, + marginBottom: 10, + justifyContent: 'space-between', + alignItems: 'center', + backgroundColor: 'rgba(255,255,255, 0.1)', + transition: 'all 0.2s ease-in-out' + }, + tileAltIconContainer: { + display: 'flex', + flex: 1, + paddingLeft: 17, + paddingRight: 17, + alignItems: 'center', + cursor: 'pointer' + }, + tileAltTypeContainer: { + display: 'flex', + flex: 8, + flexDirection: 'column', + textAlign: 'left', + justifyContent: 'flex-start' + }, + tile: { + display: 'flex', + flexDirection: 'column', + width: 331, + height: 104, + borderRadius: 8, + marginBottom: 16, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(255,255,255, 0.1)', + cursor: 'pointer', + transition: 'all 0.2s ease-in-out', + '&:hover': { + boxShadow: '0 1px 2px 1px rgba(0,0,0,0.3)', + transition: 'all 0.2s ease-in-out', + }, + '&:active': { + boxShadow: 'none', + transition: 'all 0.2s ease-in-out', + } + }, + tileSub: { + display: 'flex', + fontSize: 16, + fontWeight: 900 + }, + tileText: { + display: 'flex', + textAlign: 'center', + fontSize: 12 + }, + button: { + marginBottom: '1rem', + textAlign: 'center', + }, + buttonNoEffects: { + display: 'flex', + marginBottom: '1rem', + textAlign: 'center', + padding: 0, + backgroundColor: 'transparent', + '&:hover': { + backgroundColor: 'transparent' + }, + '&:active': { + backgroundColor: 'transparent' + } + }, + loaderContainer: { + flex: 1, + display: 'flex', + justifyContent: 'center', + alignItems: 'center' + }, + loader: {}, + buttonContainer: { + padding: '1rem', + borderTop: 'solid 1px rgba(255,255,255,0.1)' + }, + createProject: { + paddingLeft: 10, + paddingRight: 10, + width: 331, + height: 48, + backgroundColor: 'rgba(255, 255, 255, 0.1)' + }, + preUploadList: { + height: 150, + overflow: 'scroll', + scrollBehavior: 'smooth', + scrollbarWidth: 3 + } +}) + +const CreateRoundDialog = ({ + toggleCreateRoundDialog, + defaultRoundCreate, + isShowingCreateRoundDialog, + user +}) => { + const firebase = useContext(FirebaseContext); + const classes = styles() + const uploadInputRef = useRef() + const [showLoader, setShowLoader] = useState(false) + const [showUploadSound, setShowUploadSound] = useState(false) + const [preUploaded, setPreUploaded] = useState([]) + + const processFiles = (files) => { + if (files && files[0]) { + const filesArray = Array.from(files) + const newPreUploaded = preUploaded ? [...preUploaded] : [] + filesArray.forEach((file) => { + const fileType = file.type + if (/**fileType.includes('aiff') || */fileType.includes('wav')) { + const forPlay = URL.createObjectURL(file) + if (file.size <= 20000000) { + const newSound = { + name: file.name, + type: file.type, + isPlaying: false, + file, + duration: 0, + forPlay + } + newPreUploaded.push(newSound) + } else alert(`file ${file.name} is too large!`) + } else alert(`file ${file.name} is not supported!`) + }) + setPreUploaded(newPreUploaded) + } + } + + useEffect(() => { + setUpEventListeners() + }) + + const setUpEventListeners = () => { + /** Stop default behaviour on drag related events */ + const events = ['dragenter', 'dragover', 'dragleave', 'drop'] + events.forEach((event) => { + document.addEventListener(event, (e) => { + e.preventDefault() + e.stopPropagation() + }) + }) + } + + const onClose = (all) => { + if (!showUploadSound || all) { + toggleCreateRoundDialog() + } + setShowLoader(false) + setPreUploaded(null) + setShowUploadSound(false) + } + + const soundPreLoad = (i) => { + const newPreUploaded = cloneDeep(preUploaded) + const isPlaying = !newPreUploaded[i].isPlaying + newPreUploaded[i].isPlaying = isPlaying + const sound = new Audio(newPreUploaded[i].forPlay) + sound.addEventListener('loadedmetadata', () => { + const duration = sound.duration + newPreUploaded[i].duration = duration + setPreUploaded(newPreUploaded) + + }) + } + + const soundPlaybackToggle = (i) => { + const newPreUploaded = cloneDeep(preUploaded) + const isPlaying = !newPreUploaded[i].isPlaying + newPreUploaded[i].isPlaying = isPlaying + const sound = new Audio(newPreUploaded[i].forPlay) + if (isPlaying) + sound.play().catch(e => { + if (e.message.indexOf('supported')) + /** temp alert TODO: use proper user friendly alerts */ + alert('browser doesn\'t support aiff files') + else + alert('an error occured white trying to playback sound') + }) + else + sound.pause() + setPreUploaded(newPreUploaded) + } + + const createSamples = (urls) => { + return new Promise(async resolve => { + const newSamples = [] + urls && Array.isArray(urls) && urls.forEach(async url => { + let sample = getDefaultSample(user.id) + sample.remoteURL = url + sample.displayName = 'Custom' + await firebase.createSample(sample) + newSamples.push(sample) + if (newSamples.length === urls.length) { + resolve(newSamples) + } + }) + }) + + } + + const onUploadSound = async () => { + setShowLoader(true) + const urls = await firebase.uploadSound(user.id, preUploaded) + const samples = await createSamples(urls) + setShowLoader(false) + defaultRoundCreate(null, samples) + onClose(true) + } + + const convertHMS = (value) => { + const sec = parseFloat(value) + let hours = Math.floor(sec / 3600) + let minutes = Math.floor((sec - (hours * 3600)) / 60) + let seconds = Math.floor(sec - (hours * 3600) - (minutes * 60)) + let miliseconds = parseInt((sec - (hours * 3600) - (minutes * 60) - seconds) * 1000) + if (hours < 10) { hours = "0" + hours } + if (minutes < 10) { minutes = "0" + minutes } + if (seconds < 10) { seconds = "0" + seconds } + if (miliseconds < 10) { miliseconds = "0" + miliseconds } + return hours + ':' + minutes + ':' + seconds + ':' + miliseconds + } + + const trashSound = (index) => { + const newPreUploaded = cloneDeep(preUploaded) + newPreUploaded.splice(index, 1) + if (newPreUploaded.length) + setPreUploaded(newPreUploaded) + else setPreUploaded(null) + } + + return ( + onClose(true)} + aria-labelledby="simple-dialog-title" + open={isShowingCreateRoundDialog} + > + + + + onClose()}> + + + + + {showUploadSound ? 'Upload custom sounds' : 'New Project'} + + + + + {!showLoader && !showUploadSound ? + <> + { + setShowLoader(true) + defaultRoundCreate(() => { + toggleCreateRoundDialog() + setShowLoader(false) + }) + }} + className={classes.tile} + > + + + + + Feeling Lucky + + + Random sound from 3 different stock instruments + + + setShowUploadSound(true)} className={classes.tile}> + + + + + My Sounds + + + Upload audio files + + + : + showUploadSound && !showLoader ? + <> + { + + { + const files = e.target.files + processFiles(files) + }} + multiple + type='file' + /> + { + e.preventDefault() + e.stopPropagation() + const dt = e.dataTransfer + const files = dt.files + processFiles(files) + }} + onClick={(e) => { + e.stopPropagation() + uploadInputRef.current.click() + }} + style={{ height: preUploaded && preUploaded.length ? 104 : 252 }} + > + + {preUploaded && preUploaded.length ? : } + + + {preUploaded && preUploaded.length ? 'Add more sounds' : 'Choose audio files or drag and drop'} + + + .aif or .wav + + + { + preUploaded && preUploaded.length && + + {preUploaded.map((item, i) => { + const name = item.name + const nameLength = name.length + if (!item.duration) + soundPreLoad(i) + return ( + + soundPlaybackToggle(i)} className={classes.tileAltIconContainer} style={{ justifyContent: 'flex-start' }}> + + + + {nameLength > 25 ? name.substring(0, 25) + '...' : name} + {convertHMS(item.duration)} + + trashSound(i)} className={classes.tileAltIconContainer} style={{ justifyContent: 'flex-end' }}> + + + + ) + })} + + } + + } + : + + + + } + + { + showUploadSound && + + + } + + ) +} + +const mapStateToProps = state => { + return { + user: state.user, + rounds: state.rounds, + selectedRoundId: state.display.selectedRoundId, + isShowingCreateRoundDialog: state.display.isShowingCreateRoundDialog, + }; +}; + +export default connect( + mapStateToProps, + { + setRedirectAfterSignIn, + setRounds, + setIsShowingDeleteRoundDialog, + setIsShowingRenameDialog, + setSelectedRoundId + } +)(CreateRoundDialog); diff --git a/src/components/dialogs/CustomInstrumentDialog.js b/src/components/dialogs/CustomInstrumentDialog.js new file mode 100644 index 0000000..538c8e3 --- /dev/null +++ b/src/components/dialogs/CustomInstrumentDialog.js @@ -0,0 +1,487 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import React, { useState, useRef, useEffect, useContext } from 'react' +import { connect } from 'react-redux' +import { FirebaseContext } from '../../firebase'; + +import { Button, IconButton, Typography, TextField } from '@material-ui/core' +import Close from '../play/layer-settings/resources/Close' +import DialogTitle from '@material-ui/core/DialogTitle' +import Dialog from '@material-ui/core/Dialog' + +import { + setRedirectAfterSignIn, + setRounds, + setIsShowingDeleteRoundDialog, + setIsShowingRenameDialog, + setSelectedRoundId +} from '../../redux/actions' + +import { getDefaultSample } from '../../utils/defaultData' + + +import { Box, makeStyles } from '@material-ui/core' +import Upload from '../play/layer-settings/resources/Upload' +import Loader from 'react-loader-spinner' +import Add from '../play/layer-settings/resources/Add' +import Playback from '../play/layer-settings/resources/Playback' +import Trash from '../play/layer-settings/resources/Trash' +import { cloneDeep } from 'lodash' + +const styles = makeStyles({ + paper: { + display: 'flex', + flexDirection: 'column', + borderRadius: 8, + width: 363 + }, + title: { + position: 'relative', + textAlign: 'center', + fontSize: 20, + }, + titleSub: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between' + }, + body: { + display: 'flex', + flexDirection: 'column', + height: 283, + padding: '1rem', + paddingBottom: 0, + overflow: 'hidden', + borderTop: 'solid 1px rgba(255,255,255,0.1)' + }, + close: { + width: 17.78, + height: 17.78, + cursor: 'pointer' + }, + closeContainer: { + position: 'absolute', + bottom: 5, + flex: 1, + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center', + cursor: 'pointer' + }, + titleText: { + flex: 1, + display: 'flex', + justifyContent: 'center', + textAlign: 'center', + marginLeft: 5 + }, + tileAlt: { + display: 'flex', + flexDirection: 'row', + width: 331, + height: 48, + borderRadius: 12, + marginBottom: 10, + justifyContent: 'space-between', + alignItems: 'center', + backgroundColor: 'rgba(255,255,255, 0.1)', + transition: 'all 0.2s ease-in-out' + }, + tileAltIconContainer: { + display: 'flex', + flex: 1, + paddingLeft: 17, + paddingRight: 17, + alignItems: 'center', + cursor: 'pointer' + }, + tileAltTypeContainer: { + display: 'flex', + flex: 8, + flexDirection: 'column', + textAlign: 'left', + justifyContent: 'flex-start' + }, + tile: { + display: 'flex', + flexDirection: 'column', + width: 331, + height: 104, + borderRadius: 8, + marginBottom: 16, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'rgba(255,255,255, 0.1)', + cursor: 'pointer', + marginTop: 15, + transition: 'all 0.2s ease-in-out', + '&:hover': { + boxShadow: '0 1px 2px 1px rgba(0,0,0,0.3)', + transition: 'all 0.2s ease-in-out', + }, + '&:active': { + boxShadow: 'none', + transition: 'all 0.2s ease-in-out', + } + }, + tileSub: { + display: 'flex', + fontSize: 16, + fontWeight: 900 + }, + tileText: { + display: 'flex', + textAlign: 'center', + fontSize: 12 + }, + button: { + marginBottom: '1rem', + textAlign: 'center', + }, + buttonNoEffects: { + display: 'flex', + marginBottom: '1rem', + textAlign: 'center', + padding: 0, + backgroundColor: 'transparent', + '&:hover': { + backgroundColor: 'transparent' + }, + '&:active': { + backgroundColor: 'transparent' + } + }, + loaderContainer: { + flex: 1, + display: 'flex', + justifyContent: 'center', + alignItems: 'center' + }, + loader: {}, + buttonContainer: { + padding: '1rem', + borderTop: 'solid 1px rgba(255,255,255,0.1)' + }, + createProject: { + paddingLeft: 10, + paddingRight: 10, + width: 331, + height: 48, + backgroundColor: 'rgba(255, 255, 255, 0.1)' + }, + preUploadList: { + height: 150, + overflow: 'scroll', + scrollBehavior: 'smooth', + scrollbarWidth: 3 + }, + inputContainer: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingLeft: 12, + paddingRight: 12, + paddingTop: 5, + paddingBottom: 5, + borderRadius: 12, + border: '1px solid rgba(255, 255, 255, 0.1)', + } +}) + +const CustomInstrumentDialog = ({ + toggleCustomInstrumentDialog, + addInstrumentToRound, + isShowingCustomInstrumentDialog, + user +}) => { + const firebase = useContext(FirebaseContext); + const classes = styles() + const uploadInputRef = useRef() + const [showLoader, setShowLoader] = useState(false) + const [showUploadSound, setShowUploadSound] = useState(false) + const [preUploaded, setPreUploaded] = useState([]) + const [instrumentName, setInsrumentName] = useState('') + + const processFiles = (files) => { + if (files && files[0]) { + const filesArray = Array.from(files) + const newPreUploaded = preUploaded ? [...preUploaded] : [] + filesArray.forEach((file) => { + const fileType = file.type + if (/**fileType.includes('aiff') || */fileType.includes('wav')) { + const forPlay = URL.createObjectURL(file) + if (file.size <= 20000000) { + const newSound = { + name: file.name, + type: file.type, + isPlaying: false, + file, + duration: 0, + forPlay + } + newPreUploaded.push(newSound) + } else alert(`file ${file.name} is too large!`) + } else alert(`file ${file.name} is not supported!`) + }) + setPreUploaded(newPreUploaded) + } + } + + useEffect(() => { + setUpEventListeners() + }) + + const setUpEventListeners = () => { + /** Stop default behaviour on drag related events */ + const events = ['dragenter', 'dragover', 'dragleave', 'drop'] + events.forEach((event) => { + document.addEventListener(event, (e) => { + e.preventDefault() + e.stopPropagation() + }) + }) + } + + const onClose = (all) => { + if (!showUploadSound || all) { + toggleCustomInstrumentDialog() + } + setShowLoader(false) + setPreUploaded(null) + setShowUploadSound(false) + } + + const soundPreLoad = (i) => { + const newPreUploaded = cloneDeep(preUploaded) + const isPlaying = !newPreUploaded[i].isPlaying + newPreUploaded[i].isPlaying = isPlaying + const sound = new Audio(newPreUploaded[i].forPlay) + sound.addEventListener('loadedmetadata', () => { + const duration = sound.duration + newPreUploaded[i].duration = duration + setPreUploaded(newPreUploaded) + }) + } + + const soundPlaybackToggle = (i) => { + const newPreUploaded = cloneDeep(preUploaded) + const isPlaying = !newPreUploaded[i].isPlaying + newPreUploaded[i].isPlaying = isPlaying + const sound = new Audio(newPreUploaded[i].forPlay) + if (isPlaying) + sound.play().catch(e => { + if (e.message.indexOf('supported')) + /** temp alert TODO: use proper user friendly alerts */ + alert('browser doesn\'t support aiff files') + else + alert('an error occured white trying to playback sound') + }) + else + sound.pause() + setPreUploaded(newPreUploaded) + } + + const createSamples = (urls) => { + return new Promise(async resolve => { + const newSamples = [] + urls && Array.isArray(urls) && urls.forEach(async url => { + let sample = getDefaultSample(user.id) + sample.remoteURL = url + sample.displayName = instrumentName || 'Custom' + await firebase.createSample(sample) + newSamples.push(sample) + if (newSamples.length === urls.length) { + resolve(newSamples) + } + }) + }) + + } + + const onUploadSound = async () => { + setShowLoader(true) + const urls = await firebase.uploadSound(user.id, preUploaded) + const samples = await createSamples(urls) + setShowLoader(false) + addInstrumentToRound(samples) + onClose(true) + } + + const convertHMS = (value) => { + const sec = parseFloat(value) + let hours = Math.floor(sec / 3600) + let minutes = Math.floor((sec - (hours * 3600)) / 60) + let seconds = Math.floor(sec - (hours * 3600) - (minutes * 60)) + let miliseconds = parseInt((sec - (hours * 3600) - (minutes * 60) - seconds) * 1000) + if (hours < 10) { hours = "0" + hours } + if (minutes < 10) { minutes = "0" + minutes } + if (seconds < 10) { seconds = "0" + seconds } + if (miliseconds < 10) { miliseconds = "0" + miliseconds } + return hours + ':' + minutes + ':' + seconds + ':' + miliseconds + } + + const trashSound = (index) => { + const newPreUploaded = cloneDeep(preUploaded) + newPreUploaded.splice(index, 1) + if (newPreUploaded.length) + setPreUploaded(newPreUploaded) + else setPreUploaded(null) + } + + const setCurrentInstrumentName = (e) => { + const name = e.target.value + if (name.length <= 15) + setInsrumentName(name) + } + + return ( + onClose(true)} + aria-labelledby="simple-dialog-title" + open={isShowingCustomInstrumentDialog} + > + + + + onClose()}> + + + + + Upload custom sounds + + + + + {!showLoader ? + <> + + + + + Instrument Name + + + { }} /> + + + + Enter a short name + + + {instrumentName.length} + / + 15 + + + + { + const files = e.target.files + processFiles(files) + }} + multiple + type='file' + /> + { + e.preventDefault() + e.stopPropagation() + const dt = e.dataTransfer + const files = dt.files + processFiles(files) + }} + onClick={(e) => { + e.stopPropagation() + uploadInputRef.current.click() + }} + style={{ height: preUploaded && preUploaded.length ? 104 : 147 }} + > + + {preUploaded && preUploaded.length ? : } + + + {preUploaded && preUploaded.length ? 'Add more sounds' : 'Choose audio files or drag and drop'} + + + .aif or .wav + + + { + preUploaded && preUploaded.length > 0 && + + {preUploaded.map((item, i) => { + const name = item.name + const nameLength = name.length + if (!item.duration) + soundPreLoad(i) + return ( + + soundPlaybackToggle(i)} className={classes.tileAltIconContainer} style={{ justifyContent: 'flex-start' }}> + + + + {nameLength > 25 ? name.substring(0, 25) + '...' : name} + {convertHMS(item.duration)} + + trashSound(i)} className={classes.tileAltIconContainer} style={{ justifyContent: 'flex-end' }}> + + + + ) + })} + + } + + : + + + + } + + + + + + ) +} + +const mapStateToProps = state => { + return { + user: state.user, + rounds: state.rounds, + selectedRoundId: state.display.selectedRoundId, + isShowingCustomInstrumentDialog: state.display.isShowingCustomInstrumentDialog, + }; +}; + +export default connect( + mapStateToProps, + { + setRedirectAfterSignIn, + setRounds, + setIsShowingDeleteRoundDialog, + setIsShowingRenameDialog, + setSelectedRoundId + } +)(CustomInstrumentDialog); diff --git a/src/components/dialogs/ShareDialog.js b/src/components/dialogs/ShareDialog.js index f9f43a4..8769015 100644 --- a/src/components/dialogs/ShareDialog.js +++ b/src/components/dialogs/ShareDialog.js @@ -20,7 +20,10 @@ const styles = makeStyles({ borderTop: 'solid 1px rgba(255,255,255,0.1)' }, linkContainer: { - display: 'flex' + display: 'flex', + }, + paper: { + borderRadius: 8, }, QRCodeContainer: { display: 'flex', @@ -29,7 +32,10 @@ const styles = makeStyles({ padding: '1rem' }, textField: { - marginRight: '1rem' + marginRight: '1rem', + [`& fieldset`]: { + borderRadius: 8, + }, }, copyButton: { marginRight: '1rem', @@ -53,7 +59,7 @@ const ShareDialog = ({ round, isShowingShareDialog, setIsShowingShareDialog, set let fullUrl = window.location.origin + '/play/' + round.id; //if (window.location.hostname === 'localhost') return null if (window.location.hostname === 'localhost') { - fullUrl = 'http://192.168.1.123:3000/play/' + round.id; + fullUrl = 'http://192.168.136.154:3000/play/' + round.id; } if (_.isEmpty(round.shortLink)) { @@ -119,28 +125,25 @@ const ShareDialog = ({ round, isShowingShareDialog, setIsShowingShareDialog, set const classes = styles(); return ( - <> - - Share project - -

Use the QR code or link to join the collaboration.

- - - - - - - -
- -
- + + Share project + +

Use the QR code or link to join the collaboration.

+ + + + + + + +
+
); } const mapStateToProps = state => { diff --git a/src/components/dialogs/SignInDialog.js b/src/components/dialogs/SignInDialog.js index e29ceec..5127168 100644 --- a/src/components/dialogs/SignInDialog.js +++ b/src/components/dialogs/SignInDialog.js @@ -16,6 +16,9 @@ import { getRandomColor } from '../../utils/index' import _ from 'lodash' const styles = makeStyles({ + paper: { + borderRadius: 8 + }, title: { textAlign: 'center' }, @@ -33,7 +36,14 @@ const styles = makeStyles({ }, emailFormItem: { marginBottom: '1rem', - minWidth: '300px' + minWidth: '300px', + [`& fieldset`]: { + borderRadius: 8, + backgroundColor: 'transparent', + }, + }, + input: { + borderRadius: 8, }, signUpButton: { fontWeight: 600 @@ -112,7 +122,6 @@ const SignInDialog = ({ isShowingSignInDialog, setIsShowingSignInDialog, setSign await firebaseContext.auth.signInWithEmailAndPassword(email, password) onClose() } catch (e) { - console.log('email error', e); setErrorMessage(e.message) } } @@ -125,7 +134,6 @@ const SignInDialog = ({ isShowingSignInDialog, setIsShowingSignInDialog, setSign try { const authResult = await firebaseContext.auth.createUserWithEmailAndPassword(email, password) const authUser = authResult.user - console.log('authResult', authResult); let user = { displayName, email: email, @@ -133,13 +141,11 @@ const SignInDialog = ({ isShowingSignInDialog, setIsShowingSignInDialog, setSign color: getRandomColor(), isGuest: false, } - console.log('creating user', user); await firebaseContext.createUser(user) setUser(user) onClose() } catch (e) { - console.log('email error', e); setErrorMessage(e.message) } } else { @@ -162,22 +168,17 @@ const SignInDialog = ({ isShowingSignInDialog, setIsShowingSignInDialog, setSign if (!_.isEmpty(displayName)) { try { const authResult = await firebaseContext.auth.signInAnonymously() - console.log('authResult', authResult); const authUser = authResult.user - console.log('authUser', authUser); let user = { isGuest: true, displayName, id: authUser.uid, color: getRandomColor() } - console.log('creating user', user); await firebaseContext.createUser(user) setUser(user) onClose() - } catch (e) { - console.log('email error', e); setErrorMessage(e.message) } } else { @@ -195,87 +196,158 @@ const SignInDialog = ({ isShowingSignInDialog, setIsShowingSignInDialog, setSign const classes = styles(); return ( - <> - - { - !isShowingEmailForm && !isShowingEmailSignupForm && !isShowingUseAsGuestForm && - <> - + + { + !isShowingEmailForm && !isShowingEmailSignupForm && !isShowingUseAsGuestForm && + <> + - Sign in - - - - - -

Don't have an account yet?

- -
- - } - { - isShowingEmailForm && - <> - - - Sign in with email - -
- - - { - errorMessage && -

{errorMessage}

- } - - + Sign in +
+ + + + +

Don't have an account yet?

+ +
+ + } + { + isShowingEmailForm && + <> + + + Sign in with email + +
+ + + { + errorMessage && +

{errorMessage}

+ } + - + Sign in + + -
- - } - { - isShowingEmailSignupForm && - <> - - - Sign up with email - -
- - - - { - errorMessage && -

{errorMessage}

- } - - +
+ + } + { + isShowingUseAsGuestForm && + <> + + + Continue as guest + +
+ + { + errorMessage && +

{errorMessage}

+ } + +

Don't have an account yet?

+ + +
+ + } + { + isShowingEmailSignupForm && + <> + + + Sign up with email + +
+ + + + { + errorMessage && +

{errorMessage}

+ } + + -
- - } + + + } -
- + ); } diff --git a/src/components/header/Header.js b/src/components/header/Header.js index 1d54220..16ca7de 100644 --- a/src/components/header/Header.js +++ b/src/components/header/Header.js @@ -5,12 +5,11 @@ import Button from '@material-ui/core/Button'; import IconButton from '@material-ui/core/IconButton'; import Box from '@material-ui/core/Box'; import { - Link, withRouter + Link, withRouter } from "react-router-dom"; import { withStyles } from '@material-ui/core/styles'; import ShareIcon from '@material-ui/icons/Share'; -import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos'; -import PlayButton from './PlayButton'; +import { BackButton } from '../play/layer-settings/resources'; import { setUser, setIsShowingSignInDialog, setRedirectAfterSignIn, setRounds, setUserDisplayName, setSignUpDisplayName, setIsShowingShareDialog } from '../../redux/actions' import _ from 'lodash' import HeaderAvatar from './HeaderAvatar' @@ -21,213 +20,224 @@ import { FirebaseContext } from '../../firebase'; import { getRandomColor } from '../../utils/index' import CustomSamples from '../../audio-engine/CustomSamples' import { createRound } from '../../utils/index' +import { Typography } from '@material-ui/core'; const styles = theme => ({ - root: { - height: '64px', - width: '100%', - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingLeft: '1rem', - paddingRight: '1rem', - position: 'fixed', - zIndex: 4, - backgroundColor: 'rgba(47,47,47,0.9)', - }, - rightSide: { - display: 'flex', - alignItems: 'center' - }, - rightSideChild: { - marginRight: '1rem', - }, - roundAroundLogoButton: { - fontWeight: 600 - }, - avatars: { - display: 'flex', - marginRight: '1rem', - alignItems: 'center' - }, - avatar: { - position: 'relative', + root: { + height: '64px', + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingLeft: '1rem', + paddingRight: '1rem', + position: 'fixed', + zIndex: 4, + backgroundColor: 'rgba(47,47,47,0.9)', + }, + rightSide: { + display: 'flex', + alignItems: 'center' + }, + rightSideChild: { + marginRight: '1rem', + }, + roundAroundLogoButton: { + fontWeight: 600 + }, + avatars: { + display: 'flex', + marginRight: '1rem', + alignItems: 'center' + }, + shareButton: { + backgroundColor: theme.palette.secondary.main, + marginRight: '1rem' + }, + avatar: { + position: 'relative', - } + } }) class Header extends Component { - static contextType = FirebaseContext; - constructor (props) { - super(props); - this.onSignInClick = this.onSignInClick.bind(this) - this.onShareClick = this.onShareClick.bind(this) - } + static contextType = FirebaseContext; + constructor(props) { + super(props); + this.onSignInClick = this.onSignInClick.bind(this) + this.onShareClick = this.onShareClick.bind(this) + } - componentDidMount () { - const _this = this - _this.context.onUserUpdatedObservers.push(async (authUser) => { - if (!_.isNil(authUser)) { - // see if this user exists in users collection, if not then we're probably in the middle of signing up so ignore - let user = await _this.context.loadUser(authUser.uid) - if (!_.isNil(user)) { - _this.props.setUser(user) - //if (!user.emailVerified) { - // console.log('need to verify email'); - // } else { - const rounds = await _this.context.getRoundsList(user.id, 1.5) - _this.props.setRounds(rounds) - const samples = await _this.context.getSamples(user.id) - for (let sample of samples) { - CustomSamples.add(sample) - } - //console.log('CustomSamples', CustomSamples.samples); + componentDidMount() { + const _this = this + _this.context.onUserUpdatedObservers.push(async (authUser) => { + if (!_.isNil(authUser)) { + // see if this user exists in users collection, if not then we're probably in the middle of signing up so ignore + let user = await _this.context.loadUser(authUser.uid) + if (!_.isNil(user)) { + _this.props.setUser(user) + //if (!user.emailVerified) { + // console.log('need to verify email'); + // } else { + const rounds = await _this.context.getRoundsList(user.id, 1.5) + _this.props.setRounds(rounds) + const samples = await _this.context.getSamples(user.id) + for (let sample of samples) { + CustomSamples.add(sample) + } + //console.log('CustomSamples', CustomSamples.samples); - // } - } else { - ///console.log('ignoring auth change, probably signing up'); - //new user, create user document - user = { - displayName: authUser.displayName, - email: authUser.email, - avatar: authUser.photoURL, - id: authUser.uid, - color: getRandomColor(), - isGuest: false, - } - //console.log('creating user', user); - await _this.context.createUser(user) - _this.props.setUser(user) - } - // console.log('redirectAfterSignIn', _this.props.redirectAfterSignIn); - if (!_.isNil(_this.props.redirectAfterSignIn)) { - _this.redirect(authUser) - } - } else { - // console.log('signed out', _this.props.location.pathname); - if (_this.props.location.pathname !== '/') { - // _this.props.history.push('/') - _this.props.setIsShowingSignInDialog(true) - } - } - }) - } + // } + } else { + ///console.log('ignoring auth change, probably signing up'); + //new user, create user document + user = { + displayName: authUser.displayName, + email: authUser.email, + avatar: authUser.photoURL, + id: authUser.uid, + color: getRandomColor(), + isGuest: false, + } + //console.log('creating user', user); + await _this.context.createUser(user) + _this.props.setUser(user) + } + // console.log('redirectAfterSignIn', _this.props.redirectAfterSignIn); + if (!_.isNil(_this.props.redirectAfterSignIn)) { + _this.redirect(authUser) + } + } else { + // console.log('signed out', _this.props.location.pathname); + if (_this.props.location.pathname !== '/') { + // _this.props.history.push('/') + _this.props.setIsShowingSignInDialog(true) + } + } + }) + } - redirect = async (authUser) => { - //console.log('redirect', this.props.redirectAfterSignIn, authUser); - if (!authUser.isAnonymous) { - // if not guest user go to rounds list - this.props.history.push(this.props.redirectAfterSignIn) - this.props.setRedirectAfterSignIn(null) - } else if (this.props.redirectAfterSignIn === '/rounds') { - // guest user, create a new round and redirect to there instead of /rounds - let newRound = createRound(this.props.user.id) - let newRounds = [...this.props.rounds, newRound] - await this.context.createRound(newRound) - this.props.setRounds(newRounds) - this.props.setRedirectAfterSignIn(null) - this.props.history.push('/play/' + newRound.id) - } - } + redirect = async (authUser) => { + //console.log('redirect', this.props.redirectAfterSignIn, authUser); + if (!authUser.isAnonymous) { + // if not guest user go to rounds list + this.props.history.push(this.props.redirectAfterSignIn) + this.props.setRedirectAfterSignIn(null) + } else if (this.props.redirectAfterSignIn === '/rounds') { + // guest user, create a new round and redirect to there instead of /rounds + let newRound = await createRound(this.props.user.id) + if (!newRound) return; + let newRounds = [newRound, ...this.props.rounds] + await this.context.createRound(newRound) + this.props.setRounds(newRounds) + this.props.setRedirectAfterSignIn(null) + this.props.history.push('/play/' + newRound.id) + } + } - onSignInClick = () => { - this.props.setIsShowingSignInDialog(true) - } + onSignInClick = () => { + this.props.setIsShowingSignInDialog(true) + } - onShareClick = () => { - this.props.setIsShowingShareDialog(true) - } + onShareClick = () => { + this.props.setIsShowingShareDialog(true) + } - render () { - const { classes, location, round, users, user } = this.props; - const isPlayMode = location.pathname.includes('/play/') ? true : false - return ( - - {isPlayMode && - <> -
- - - -
-
- { - round && -
- } - { - _.isNil(round) && -
Loading...
+ render() { + const { classes, location, round, users, user } = this.props; + const isPlayMode = location.pathname.includes('/play/') ? true : false + return ( + + {isPlayMode && + <> + + + + + + { + round && + + + + } + { + _.isNil(round) && + Loading... - } -
- - - { - users.map((currentUser) => ( - - )) - } - - -
- -
-
- -
-
- -
-
- - } - {!isPlayMode && - <> -
-
- { - user && - - } - { - !user && - - } + } +
+ + + + { + users.map((currentUser) => ( + + )) + } + + {users.length > 1 && } + + + + + + + + + } + {!isPlayMode && + <> + + + + { + user && + + } + { + !user && + + } - - } - - - - ) - } + + } + + ) + } } Header.propTypes = { - classes: PropTypes.object.isRequired, + classes: PropTypes.object.isRequired, }; const mapStateToProps = state => { - return { - user: state.user, - users: state.users, - redirectAfterSignIn: state.display.redirectAfterSignIn, - signupDisplayName: state.display.signupDisplayName, - rounds: state.rounds, - round: state.round - }; + return { + user: state.user, + users: state.users, + redirectAfterSignIn: state.display.redirectAfterSignIn, + signupDisplayName: state.display.signupDisplayName, + rounds: state.rounds, + round: state.round + }; }; export default connect( - mapStateToProps, - { - setUser, - setUserDisplayName, - setSignUpDisplayName, - setIsShowingSignInDialog, - setRedirectAfterSignIn, - setRounds, - setIsShowingShareDialog - } + mapStateToProps, + { + setUser, + setUserDisplayName, + setSignUpDisplayName, + setIsShowingSignInDialog, + setRedirectAfterSignIn, + setRounds, + setIsShowingShareDialog + } )(withRouter((withStyles(styles)(Header)))); \ No newline at end of file diff --git a/src/components/header/HeaderAvatar.js b/src/components/header/HeaderAvatar.js index 8d08397..0b81d3a 100644 --- a/src/components/header/HeaderAvatar.js +++ b/src/components/header/HeaderAvatar.js @@ -14,198 +14,210 @@ import IconButton from '@material-ui/core/IconButton'; import _ from 'lodash' import { FirebaseContext } from '../../firebase'; import { - setUser, setRounds, setUsers, setRound, setUserColor + setUser, setRounds, setUsers, setRound, setUserColor } from '../../redux/actions' import { CirclePicker } from 'react-color' import { Colors } from '../../utils/constants' const useStyles = makeStyles((theme) => ({ - root: { - display: 'flex', - }, - paper: { - marginRight: theme.spacing(2), - borderRadius: '16px' - }, - colorPicker: { - padding: '1rem' - }, - menuList: { - }, - menuListItem: { - paddingTop: '1rem', - paddingBottom: '1rem', - textAlign: 'center' - }, - avatar: props => ({ - border: 'solid 2px ' + props.userColor - }), - avatarInitialsOnly: props => ({ - backgroundColor: props.userColor, - color: '#FFFFFF' - }), - userDisplayName: { - marginTop: 0, - marginBottom: '0rem', - paddingTop: '1rem', - marginLeft: '1rem', - marginRight: '1rem' - }, - userEmail: { - marginLeft: '1rem', - marginRight: '1rem', - marginTop: 0, - fontWeight: 500 - } + root: { + display: 'flex', + }, + paper: { + marginRight: theme.spacing(2), + borderRadius: 8 + }, + colorPicker: { + padding: '1rem' + }, + menuList: { + }, + menuListItem: { + paddingTop: '1rem', + paddingBottom: '1rem', + textAlign: 'center' + }, + avatar: props => ({ + border: 'solid 2px ' + props.userColor + }), + avatarInitialsOnly: props => ({ + backgroundColor: props.userColor, + color: '#FFFFFF' + }), + userDisplayName: { + marginTop: 0, + marginBottom: '0rem', + paddingTop: '1rem', + marginLeft: '1rem', + marginRight: '1rem' + }, + userEmail: { + marginLeft: '1rem', + marginRight: '1rem', + marginTop: 0, + fontWeight: 500 + } })); -function HeaderAvatar ({ user, users, setUser, setRounds, shouldShowMenu, setUsers, setRound, setUserColor }) { - const firebaseContext = useContext(FirebaseContext) - const [open, setOpen] = React.useState(false); - const anchorRef = React.useRef(null); - - const handleToggle = () => { - setOpen((prevOpen) => !prevOpen); - }; - - const handleClose = (event) => { - if (anchorRef.current && anchorRef.current.contains(event.target)) { - return; - } - - setOpen(false); - }; - - function handleListKeyDown (event) { - if (event.key === 'Tab') { - event.preventDefault(); - setOpen(false); - } - } - - const onSignOutClick = () => { - firebaseContext.signOut() - setRounds([]) - setUser(null) - setRound(null) - setUsers([]) - } - - const getInitials = (name) => { - let initials = '??' - if (!_.isNil(name)) { - const nameParts = name.split(' ') - if (nameParts.length > 1) { - initials = nameParts[0][0] + nameParts[1][0] - } else { - initials = name[0] - if (name.length > 1) { - initials += name[1] - } - } - } - return initials - } - - const onColorChosen = ({ hex }) => { - setUserColor(hex) - firebaseContext.updateUser(user.id, { color: hex }) - let usersClone = _.cloneDeep(users) - let me = _.find(usersClone, { id: user.id }) - me.color = hex - setUsers(usersClone) - } - - // return focus to the button when we transitioned from !open -> open - const prevOpen = React.useRef(open); - React.useEffect(() => { - if (prevOpen.current === true && open === false) { - anchorRef.current.focus(); - } - - prevOpen.current = open; - }, [open]); - - const classes = useStyles({ userColor: user.color }); - - return ( -
- -
- { - shouldShowMenu && - <> - - { - !_.isNil(user.avatar) && - - } - { - _.isNil(user.avatar) && - {getInitials(user.displayName)} - } - - - - {({ TransitionProps, placement }) => ( - - - - - -

{user.displayName}

-

{user.email}

- - - - Sign out - -
-
-
-
- )} -
- - - } - { - !shouldShowMenu && - <> - { - !_.isNil(user.avatar) && - - - - } - { - _.isNil(user.avatar) && - - {getInitials(user.displayName)} - - } - - } - -
-
- ); +function HeaderAvatar({ user, users, setUser, setRounds, shouldShowMenu, setUsers, setRound, setUserColor }) { + const firebaseContext = useContext(FirebaseContext) + const [open, setOpen] = React.useState(false); + const anchorRef = React.useRef(null); + + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = (event) => { + if (anchorRef.current && anchorRef.current.contains(event.target)) { + return; + } + + setOpen(false); + }; + + function handleListKeyDown(event) { + if (event.key === 'Tab') { + event.preventDefault(); + setOpen(false); + } + } + + const onSignOutClick = () => { + firebaseContext.signOut() + setRounds([]) + setUser(null) + setRound(null) + setUsers([]) + } + + const getInitials = (name) => { + let initials = '??' + if (!_.isNil(name)) { + const nameParts = name.split(' ') + if (nameParts.length > 1) { + initials = nameParts[0][0] + nameParts[1][0] + } else { + initials = name[0] + if (name.length > 1) { + initials += name[1] + } + } + } + return initials + } + + const onColorChosen = ({ hex }) => { + setUserColor(hex) + firebaseContext.updateUser(user.id, { color: hex }) + let usersClone = _.cloneDeep(users) + let me = _.find(usersClone, { id: user.id }) + me.color = hex + setUsers(usersClone) + } + + // return focus to the button when we transitioned from !open -> open + const prevOpen = React.useRef(open); + React.useEffect(() => { + if (prevOpen.current === true && open === false) { + anchorRef.current.focus(); + } + + prevOpen.current = open; + }, [open]); + + const classes = useStyles({ userColor: user.color }); + + return ( + + + { + shouldShowMenu && + <> + + { + !_.isNil(user.avatar) && + + } + { + _.isNil(user.avatar) && + {getInitials(user.displayName)} + } + + + + {({ TransitionProps, placement }) => ( + + + + + +

{user.displayName}

+

{user.email}

+ + + + + Sign out + + +
+
+
+
+ )} +
+ + + } + { + !shouldShowMenu && + <> + { + !_.isNil(user.avatar) && + + + + } + { + _.isNil(user.avatar) && + + {getInitials(user.displayName)} + + } + + } + +
+
+ ); } export default connect( - null, - { - setUser, - setUsers, - setRound, - setRounds, - setUserColor - } + null, + { + setUser, + setUsers, + setRound, + setRounds, + setUserColor + } )(HeaderAvatar); \ No newline at end of file diff --git a/src/components/header/HeaderMenu.js b/src/components/header/HeaderMenu.js index e0ee254..e7f9a07 100644 --- a/src/components/header/HeaderMenu.js +++ b/src/components/header/HeaderMenu.js @@ -22,7 +22,7 @@ const useStyles = makeStyles((theme) => ({ }, paper: { marginRight: theme.spacing(2), - borderRadius: '16px' + borderRadius: 8 }, menuList: { }, @@ -33,7 +33,7 @@ const useStyles = makeStyles((theme) => ({ } })); -export default function HeaderMenu ({ name }) { +export default function HeaderMenu({ name }) { const classes = useStyles(); const [open, setOpen] = React.useState(false); const anchorRef = React.useRef(null); @@ -50,7 +50,7 @@ export default function HeaderMenu ({ name }) { setOpen(false); }; - function handleListKeyDown (event) { + function handleListKeyDown(event) { if (event.key === 'Tab') { event.preventDefault(); setOpen(false); @@ -89,9 +89,8 @@ export default function HeaderMenu ({ name }) { }, [open]); return ( -
- -
+ + - {({ TransitionProps, placement }) => ( - + @@ -116,15 +114,13 @@ export default function HeaderMenu ({ name }) { - - )} -
-
+ + ); } diff --git a/src/components/header/HeaderOld.js b/src/components/header/HeaderOld.js index a2eeb6a..85c2dc7 100644 --- a/src/components/header/HeaderOld.js +++ b/src/components/header/HeaderOld.js @@ -8,8 +8,8 @@ import { } from "react-router-dom"; import { makeStyles } from '@material-ui/core/styles'; import ShareIcon from '@material-ui/icons/Share'; -import ArrowBackIosIcon from '@material-ui/icons/ArrowBackIos'; import PlayButton from './PlayButton'; +import { BackButton } from '../play/layer-settings/resources'; import { useLocation } from 'react-router-dom' import { setUser, setIsShowingSignInDialog, setRedirectAfterSignIn, setRounds, setUserDisplayName, setSignUpDisplayName, setIsShowingShareDialog } from '../../redux/actions' import _ from 'lodash' @@ -45,6 +45,12 @@ const headerStyles = makeStyles((theme) => ({ roundAroundLogoButton: { fontWeight: 600 }, + iconButton: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center' + }, avatars: { display: 'flex', marginRight: '1rem', @@ -56,7 +62,7 @@ const headerStyles = makeStyles((theme) => ({ } })) -function Header ({ user, users, round, setUser, setIsShowingSignInDialog, redirectAfterSignIn, setRedirectAfterSignIn, rounds, setRounds, signupDisplayName, setIsShowingShareDialog }) { +function Header({ user, users, round, setUser, setIsShowingSignInDialog, redirectAfterSignIn, setRedirectAfterSignIn, rounds, setRounds, signupDisplayName, setIsShowingShareDialog }) { const firebaseContext = useContext(FirebaseContext); const classes = headerStyles(); const location = useLocation(); @@ -115,33 +121,31 @@ function Header ({ user, users, round, setUser, setIsShowingSignInDialog, redire } }) // eslint-disable-next-line react-hooks/exhaustive-deps - - }, []) - - return ( {isPlayMode && <> -
- - + + + -
-
+ + { round && -
+ + + } { _.isNil(round) && -
Loading...
+ Loading... } -
+
{ @@ -151,22 +155,23 @@ function Header ({ user, users, round, setUser, setIsShowingSignInDialog, redire } -
+ -
-
+ + -
-
+ + -
+
} {!isPlayMode && <> -
-
+ + + { user && @@ -175,11 +180,8 @@ function Header ({ user, users, round, setUser, setIsShowingSignInDialog, redire !user && } - } - - ) } diff --git a/src/components/header/PlayButton.js b/src/components/header/PlayButton.js index 2878130..ce0f301 100644 --- a/src/components/header/PlayButton.js +++ b/src/components/header/PlayButton.js @@ -1,4 +1,4 @@ -import React, { useContext } from 'react' +import React from 'react' import { connect } from "react-redux"; import IconButton from '@material-ui/core/IconButton'; import PlayIcon from '@material-ui/icons/PlayArrowOutlined'; @@ -7,7 +7,6 @@ import { makeStyles } from '@material-ui/core/styles'; import AudioEngine from '../../audio-engine/AudioEngine' import { setIsPlaying, } from '../../redux/actions' import _ from 'lodash' -import { FirebaseContext } from '../../firebase'; const playButtonStyles = makeStyles(function (theme) { @@ -20,19 +19,15 @@ const playButtonStyles = makeStyles(function (theme) { } }) - -function PlayButton ({ isPlaying, setIsPlaying, roundId }) { - const firebase = useContext(FirebaseContext); +function PlayButton({ isPlaying, setIsPlaying }) { const onPlayClick = () => { if (isPlaying) { AudioEngine.stop() setIsPlaying(false) - firebase.updateRound(roundId, { isPlaying: false }) } else { AudioEngine.play() setIsPlaying(true) - firebase.updateRound(roundId, { isPlaying: true }) } } const classes = playButtonStyles(); @@ -46,8 +41,6 @@ function PlayButton ({ isPlaying, setIsPlaying, roundId }) { isPlaying && } - - ) } diff --git a/src/components/header/ProjectName.js b/src/components/header/ProjectName.js index 6cbd505..ce9bfce 100644 --- a/src/components/header/ProjectName.js +++ b/src/components/header/ProjectName.js @@ -13,20 +13,26 @@ import _ from 'lodash' import { uuid } from '../../utils/index'; import { setRound, setIsShowingRenameDialog, setIsShowingDeleteRoundDialog, setSelectedRoundId } from '../../redux/actions'; import { FirebaseContext } from '../../firebase'; +import { Box } from '@material-ui/core'; const useStyles = makeStyles((theme) => ({ root: { display: 'flex', }, paper: { - marginRight: theme.spacing(2), - borderRadius: '16px' + //marginRight: theme.spacing(2), + justifySelf: 'flex-start', + width: 130, + borderRadius: 8, + [theme.breakpoints.down('xs')]: { + width: 100, + } }, menuList: { } })); -function ProjectName ({ name, setIsShowingRenameDialog, setIsShowingDeleteRoundDialog, setRound, round, setSelectedRoundId }) { +function ProjectName({ name, setIsShowingRenameDialog, setIsShowingDeleteRoundDialog, setRound, round, setSelectedRoundId }) { const classes = useStyles(); const [open, setOpen] = React.useState(false); const anchorRef = React.useRef(null); @@ -56,16 +62,16 @@ function ProjectName ({ name, setIsShowingRenameDialog, setIsShowingDeleteRoundD setSelectedRoundId(round.id) setIsShowingDeleteRoundDialog(true) } - const onDuplicateClick = () => { + const onDuplicateClick = async () => { let clonedRound = _.cloneDeep(round) clonedRound.id = uuid() clonedRound.name += ' (duplicate)' - firebase.createRound(clonedRound) + await firebase.createRound(clonedRound) setRound(clonedRound) setOpen(false) } - function handleListKeyDown (event) { + function handleListKeyDown(event) { if (event.key === 'Tab') { event.preventDefault(); setOpen(false); @@ -83,9 +89,8 @@ function ProjectName ({ name, setIsShowingRenameDialog, setIsShowingDeleteRoundD }, [open]); return ( -
- -
+ +
-
+ + ); } const mapStateToProps = state => { diff --git a/src/components/header/TempoSlider.js b/src/components/header/TempoSlider.js index 8b33a14..895cbb1 100644 --- a/src/components/header/TempoSlider.js +++ b/src/components/header/TempoSlider.js @@ -26,7 +26,7 @@ const StyledSlider = withStyles({ }, })(Slider); -function TempoSlider ({ round, setRoundBpm }) { +function TempoSlider({ round, setRoundBpm }) { const firebase = useContext(FirebaseContext); const [value, setValue] = React.useState(round.bpm); const updateTempoState = (bpm) => { @@ -44,7 +44,7 @@ function TempoSlider ({ round, setRoundBpm }) { updateTempoStateThrottled(bpm) }; - function valuetext (value) { + function valuetext(value) { return `${value}`; } return ( diff --git a/src/components/landing-page/LandingPageRoute.js b/src/components/landing-page/LandingPageRoute.js index 8d1c255..aa6e788 100644 --- a/src/components/landing-page/LandingPageRoute.js +++ b/src/components/landing-page/LandingPageRoute.js @@ -1,7 +1,6 @@ import React, { Component } from 'react' import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/styles'; -import Box from '@material-ui/core/Box'; import Container from '@material-ui/core/Container'; import Grid from '@material-ui/core/Grid'; import Button from '@material-ui/core/Button'; @@ -41,18 +40,18 @@ const styles = theme => ({ class LandingPageRoute extends Component { static contextType = FirebaseContext; - constructor (props) { + constructor(props) { super(props); this.onGetStartedClick = this.onGetStartedClick.bind(this); } - async onGetStartedClick () { + async onGetStartedClick() { if (!_.isNil(this.props.user)) { if (!this.props.user.isGuest) { // redirect to /rounds list this.props.history.push('/rounds') } else { // guest user so create new round and go there instead of rounds list - let newRound = createRound(this.props.user.id) + let newRound = await createRound(this.props.user.id) let newRounds = [newRound] await this.context.createRound(newRound) this.props.setRounds(newRounds) @@ -64,7 +63,7 @@ class LandingPageRoute extends Component { this.props.setIsShowingSignInDialog(true) } } - render () { + render() { const { classes } = this.props; return ( <> @@ -74,14 +73,22 @@ class LandingPageRoute extends Component {

Gather around, make music, and have fun.

Rounds is a multi-person live-sampling step-sequencer with social features. It runs in the browser or as a Native iOS application, with the following steps: compose a pattern (or "Round"), make variations and save presets, share a link to have someone join you with additional layers. Rounds is best on a recent iPad.

- +
diff --git a/src/components/play/EffectThumbControl.js b/src/components/play/EffectThumbControl.js index bee6c03..b8685dc 100644 --- a/src/components/play/EffectThumbControl.js +++ b/src/components/play/EffectThumbControl.js @@ -1,52 +1,88 @@ import React, { Component } from 'react' import { SVG } from '@svgdotjs/svg.js' -import { LockOpen } from '@material-ui/icons'; +//import { LockOpen, Lock } from '@material-ui/icons'; import FX from '../../audio-engine/FX' +import { Box } from '@material-ui/core'; +import OpenLock from './layer-settings/resources/svg/openLock.svg'; +import Lock from './layer-settings/resources/svg/lock.svg'; import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/styles'; const styles = theme => ({ button: { cursor: 'pointer', - '&:hover': { - opacity: 0.8 - } + }, + container: { + display: 'flex', + flexDirection: 'column', + width: '96px', + height: '48px', + borderRadius: '24px', + position: 'relative', + margin: '0.2rem', + border: '1px solid rgba(255,255,255,0.1)', + alignItems: 'center', + justifyContent: 'center', + }, + lockContainer: { + display: 'flex', + flexDirection: 'row', + position: 'absolute', + height: '100%', + justifyContent: 'center', + alignItems: 'center' + }, + open: { + display: 'flex', + color: '#474747', + zIndex: 1 + }, + locked: { + display: 'flex', + color: '#474747', + zIndex: 1 }, iconDark: { - color: '#222222' + color: '#474747' } }) -const thumbWidth = 48; -const thumbHeight = 48; -const containerWidth = thumbWidth + 40 +const thumbWidth = 32; +const thumbHeight = 32; +const containerWidth = thumbWidth + 46 class EffectThumbControl extends Component { - constructor (props) { + constructor(props) { super(props); this.thumbControlRef = React.createRef(); - this.isOn = false // this.props.isOn + this.isOn = this.props.isOn; + this.isOverride = this.props.isOverride; this.onMouseMove = this.onMouseMove.bind(this) this.onMouseUp = this.onMouseUp.bind(this) } - componentDidMount () { + componentDidMount() { const element = this.thumbControlRef.current; - + const isOn = this.props.isOn && this.props.isOverride; this.container = SVG() .addTo(element) - .size(thumbWidth + 40, thumbHeight) - this.background = this.container.rect(thumbWidth + 40, thumbHeight).fill('none').radius(24) + .size(thumbWidth + 46, thumbHeight) + this.background = this.container.rect(thumbWidth + 46, thumbHeight).fill('none').radius(24) this.thumb = this.container.nested() this.thumb.x(containerWidth - thumbWidth) this.thumb.addClass(this.props.classes.button) - this.thumbBackground = this.thumb.rect(thumbWidth, thumbHeight).fill('#474747').radius(24) + this.thumbBackground = this.thumb.rect(thumbWidth, thumbHeight).fill('#686868').radius(24) this.labelContainer = this.thumb.nested() this.label = this.labelContainer.svg(FX.getIcon(this.props.name)) this.label.x((thumbWidth / 2) - (this.label.node.getBBox().width / 2)) this.label.y((thumbHeight / 2) - (this.label.node.getBBox().height / 2)) this.addEventListeners() + + if (isOn) + this.setSwitchIsOn() + else this.setSwitchIsOff() } - addEventListeners () { + + addEventListeners() { this.thumb.on('touchstart', (e) => { e.preventDefault() this.switchOn() @@ -74,14 +110,11 @@ class EffectThumbControl extends Component { if (x > threshold) { x = containerWidth - thumbWidth this.isOn = false - this.thumbBackground.fill('#474747') - this.label.removeClass(this.props.classes.iconDark) - this.switchOff() + this.setSwitchIsOff() } else { x = 0 this.isOn = true - //this.thumbBackground.fill('#474747') - this.label.addClass(this.props.classes.iconDark) + this.setSwitchIsOn() } this.thumb.x(x) }) @@ -90,25 +123,38 @@ class EffectThumbControl extends Component { this.switchOn() this.dragStart = e.pageX this.thumbBackground.fill('#EAEAEA') - this.label.addClass(this.props.classes.iconDark) document.addEventListener('mousemove', this.onMouseMove) document.addEventListener('mouseup', this.onMouseUp) }) } - onMouseMove (e) { + onMouseMove(e) { e.preventDefault() let x = e.pageX - this.dragStart - if (!this.isOn) { - x = (containerWidth - thumbWidth) + e.pageX - this.dragStart - } - if (x > containerWidth - thumbWidth) { - x = containerWidth - thumbWidth - } else if (x < 0) { - x = 0 + // stop minute difference from being used as moves + if (x > 3 || x < -3) { + if (!this.isOn) { + x = (containerWidth - thumbWidth) + e.pageX - this.dragStart + } + if (x > containerWidth - thumbWidth) { + x = containerWidth - thumbWidth + } else if (x < 0) { + x = 0 + } + this.thumb.x(x) } + } + setSwitchIsOn = () => { + this.thumb.x(0) + this.thumbBackground.fill('#686868') + this.switchOn() + } + setSwitchIsOff = () => { + const x = containerWidth - thumbWidth this.thumb.x(x) + this.thumbBackground.fill('#555555') + this.switchOff(); } - onMouseUp (e) { + onMouseUp(e) { e.preventDefault() document.removeEventListener('mouseup', this.onMouseUp) document.removeEventListener('mousemove', this.onMouseMove) @@ -118,32 +164,34 @@ class EffectThumbControl extends Component { if (x > threshold) { x = containerWidth - thumbWidth this.isOn = false; - this.switchOff() - this.thumbBackground.fill('#474747') - this.label.removeClass(this.props.classes.iconDark) + this.setSwitchIsOff() } else { x = 0 this.isOn = true - this.label.addClass(this.props.classes.iconDark) + this.setSwitchIsOn() } this.thumb.x(x) } - switchOn () { + switchOn() { this.props.switchOn(this.props.fxId) } - switchOff () { - this.isOn = false + switchOff() { this.props.switchOff(this.props.fxId) } - render () { + render() { + const { classes } = this.props; return ( -
- -
-
-
-
- + + + open lock + + + + + + locked + + ) } } diff --git a/src/components/play/EffectsSidebar.js b/src/components/play/EffectsSidebar.js index b964756..e9e1e4d 100644 --- a/src/components/play/EffectsSidebar.js +++ b/src/components/play/EffectsSidebar.js @@ -1,19 +1,17 @@ import React, { Component } from 'react' -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/styles'; -import Box from '@material-ui/core/Box'; -import { connect } from "react-redux"; +import PropTypes from 'prop-types' +import { withStyles } from '@material-ui/styles' +import Box from '@material-ui/core/Box' +import { connect } from "react-redux" import AudioEngine from "../../audio-engine/AudioEngine" -import { FirebaseContext } from '../../firebase'; +import { FirebaseContext } from '../../firebase' import { setUserBusFx, setUserBusFxOverride -} from "../../redux/actions"; -//import { sortableContainer, sortableElement, sortableHandle } from 'react-sortable-hoc'; +} from "../../redux/actions" import arrayMove from 'array-move' -//import { DragIndicator } from '@material-ui/icons'; -import EffectThumbControl from './EffectThumbControl'; -import ChevronRightIcon from '@material-ui/icons/ChevronRight'; +import EffectThumbControl from './EffectThumbControl' +import ChevronRightIcon from '@material-ui/icons/ChevronRight' import _ from 'lodash' const styles = theme => ({ @@ -24,14 +22,24 @@ const styles = theme => ({ right: '0', top: '64px', borderTop: 'solid 1px rgba(255,255,255,0.1)', - backgroundColor: 'rgba(47,47,47,0.9)', display: 'flex', flexDirection: 'column', alignItems: 'center', - justifyContent: 'flex-end', - paddingBottom: '0.5rem', + justifyContent: 'center', transition: 'right 0.4s', }, + effectContainer: { + display: 'flex', + position: 'relative', + flexDirection: 'column', + height: 352, + width: 120, + borderTopLeftRadius: 8, + borderBottomLeftRadius: 8, + backgroundColor: 'rgba(47,47,47,0.9)', + alignItems: 'center', + justifyContent: 'center', + }, isMinimized: { right: '-120px' }, @@ -41,8 +49,8 @@ const styles = theme => ({ height: '32px', position: 'absolute', left: '-40px', - bottom: '16px', - borderRadius: '16px', + top: '12px', + borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', @@ -79,17 +87,6 @@ const styles = theme => ({ } }) -/*const DragHandle = sortableHandle(({ classes }) => ); -const SortableItem = sortableElement(({ fx, onSwitchOn, onSwitchOff, classes }) => ( -
  • - - -
  • -)); -const SortableContainer = sortableContainer(({ children, classes }) => { - return
      {children}
    ; -});*/ - const toTitleCase = (str) => { return str.replace( /\w\S*/g, @@ -102,7 +99,7 @@ const toTitleCase = (str) => { class EffectsSidebar extends Component { static contextType = FirebaseContext; - constructor (props) { + constructor(props) { super(props) this.state = { menuAnchorElement: null, @@ -113,9 +110,10 @@ class EffectsSidebar extends Component { this.onMinimizeClick = this.onMinimizeClick.bind(this) } - onPlayClick () { + onPlayClick() { this.props.togglePlay() } + onSortEnd = ({ oldIndex, newIndex }) => { let userBus = _.cloneDeep(this.props.round.userBuses[this.props.user.id]) userBus.fx = arrayMove(userBus.fx, oldIndex, newIndex) @@ -123,42 +121,40 @@ class EffectsSidebar extends Component { userBus.fx[i].order = i } this.props.setUserBusFx(this.props.user.id, userBus.fx) - //this.props.dispatch({ type: SET_USER_BUS_FX, payload: { userId: this.props.user.id, data: userBus.fx } }) AudioEngine.busesByUser[this.props.user.id].setFxOrder(userBus.fx) this.context.updateUserBus(this.props.round.id, this.props.user.id, userBus) + } - /*this.setState(({ items }) => ({ - items: arrayMove(items, oldIndex, newIndex), - }));*/ - }; - onSwitchOn (fxId) { + onSwitchOn(fxId) { + if (!this.props.user || !AudioEngine.busesByUser[this.props.user.id]) return AudioEngine.busesByUser[this.props.user.id].fx[fxId].override = true this.props.setUserBusFxOverride(this.props.user.id, fxId, true) - //this.props.dispatch({ type: SET_USER_BUS_FX_OVERRIDE, payload: { fxId, userId: this.props.user.id, value: true } }) let userBus = _.cloneDeep(this.props.round.userBuses[this.props.user.id]) let fx = _.find(userBus.fx, { id: fxId }) fx.isOverride = true this.context.updateUserBus(this.props.round.id, this.props.user.id, userBus) } - onSwitchOff (fxId) { + onSwitchOff(fxId) { + if (!this.props.user || !AudioEngine.busesByUser[this.props.user.id]) return AudioEngine.busesByUser[this.props.user.id].fx[fxId].override = false this.props.setUserBusFxOverride(this.props.user.id, fxId, false) - //this.props.dispatch({ type: SET_USER_BUS_FX_OVERRIDE, payload: { fxId, userId: this.props.user.id, value: false } }) let userBus = _.cloneDeep(this.props.round.userBuses[this.props.user.id]) let fx = _.find(userBus.fx, { id: fxId }) fx.isOverride = false this.context.updateUserBus(this.props.round.id, this.props.user.id, userBus) } - onMinimizeClick () { + onMinimizeClick() { this.setState({ isMinimized: !this.state.isMinimized }) } - render () { + render() { const { classes } = this.props; let items = [] if (!_.isNil(this.props.round) && !_.isNil(this.props.round.userBuses) && !_.isNil(this.props.round.userBuses[this.props.user.id])) { for (const fx of this.props.round.userBuses[this.props.user.id].fx) { let item = { id: fx.id, + isOn: fx.isOn, + isOverride: fx.isOverride, label: fx.name, userId: this.props.user.id, name: fx.name @@ -171,40 +167,33 @@ class EffectsSidebar extends Component { return ( - {items.map((fx, index) => ( - - ))} - - - ) - - /*return ( - - - {items.map((fx, index) => ( - + + + + + {items.map((fx) => ( + ))} - - + - )*/ + ) } } EffectsSidebar.propTypes = { classes: PropTypes.object.isRequired, -}; +} const mapStateToProps = state => { return { round: state.round, user: state.user, display: state.display - }; -}; + } +} export default connect( mapStateToProps, { setUserBusFx, setUserBusFxOverride } -)(withStyles(styles)(EffectsSidebar)); \ No newline at end of file +)(withStyles(styles)(EffectsSidebar)) \ No newline at end of file diff --git a/src/components/play/PatternSequencer.js b/src/components/play/PatternSequencer.js index f9ad677..b651fe7 100644 --- a/src/components/play/PatternSequencer.js +++ b/src/components/play/PatternSequencer.js @@ -2,7 +2,6 @@ import React, { Component } from 'react' import { connect } from "react-redux"; import { Button } from '@material-ui/core'; import { withStyles } from '@material-ui/styles'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; import Switch from '@material-ui/core/Switch'; import PropTypes from 'prop-types'; import Box from '@material-ui/core/Box'; @@ -64,12 +63,12 @@ const styles = theme => ({ class PatternSequencer extends Component { static contextType = FirebaseContext; - constructor (props) { + constructor(props) { super(props) this.onWriteClick = this.onWriteClick.bind(this) this.onPlayingSequenceToggle = this.onPlayingSequenceToggle.bind(this) } - onWriteClick () { + onWriteClick() { if (!this.props.display.isRecordingSequence) { // start write this.props.setUserPatternSequence(this.props.user.id, getDefaultUserPatternSequence()) @@ -82,23 +81,20 @@ class PatternSequencer extends Component { this.props.setIsRecordingSequence(!this.props.display.isRecordingSequence) } - onPlayingSequenceToggle (event) { - // console.log('here 1'); + onPlayingSequenceToggle(event) { this.props.setCurrentSequencePattern(0) this.props.setIsPlayingSequence(this.props.user.id, event.target.checked) - // console.log('here 2', this.props.round.userPatterns[this.props.user.id]); let userPatternsClone = _.cloneDeep(this.props.round.userPatterns[this.props.user.id]) userPatternsClone.isPlayingSequence = event.target.checked this.context.saveUserPatterns(this.props.round.id, this.props.user.id, userPatternsClone) - // console.log('here 3'); } - getPatternOrderDisplay (id) { + getPatternOrderDisplay(id) { let pattern = _.find(this.props.round.userPatterns[this.props.user.id].patterns, { id: id }) return pattern.order + 1 } - render () { + render() { const { classes } = this.props; let items = [] if (!_.isNil(this.props.round) && !_.isNil(this.props.round.userPatterns) && !_.isNil(this.props.round.userPatterns[this.props.user.id])) { @@ -125,9 +121,8 @@ class PatternSequencer extends Component { ))} - { (!_.isNil(this.props.user) && !_.isNil(this.props.round) && !_.isNil(this.props.round.userPatterns[this.props.user.id])) && + {(!_.isNil(this.props.user) && !_.isNil(this.props.round) && !_.isNil(this.props.round.userPatterns[this.props.user.id])) && <> - @@ -138,7 +133,6 @@ class PatternSequencer extends Component { /> - } diff --git a/src/components/play/PatternThumbControl.js b/src/components/play/PatternThumbControl.js index b727bb0..09df804 100644 --- a/src/components/play/PatternThumbControl.js +++ b/src/components/play/PatternThumbControl.js @@ -1,6 +1,5 @@ import React, { Component } from 'react' import { SVG } from '@svgdotjs/svg.js' -import { Save } from '@material-ui/icons'; import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/styles'; @@ -18,14 +17,15 @@ const styles = theme => ({ }) class PatternThumbControl extends Component { - constructor (props) { + constructor(props) { super(props); this.thumbControlRef = React.createRef(); this.onMouseMove = this.onMouseMove.bind(this) this.onMouseUp = this.onMouseUp.bind(this) this.isOver = false } - componentDidMount () { + componentDidMount() { + const { color } = this.props; const element = this.thumbControlRef.current; this.container = SVG() .addTo(element) @@ -38,10 +38,10 @@ class PatternThumbControl extends Component { this.label = this.thumb.plain(this.props.label) this.label.font({ family: 'Arial', - size: 14, - weight: 600 + size: 25, + weight: 900 }) - this.label.fill('#FFFFFF') + this.label.fill(color) this.label.x((thumbWidth / 2) - (this.label.node.getBBox().width / 2)) this.label.y((thumbHeight / 2) - (this.label.node.getBBox().height / 2)) this.arrowContainer = this.thumb.nested() @@ -57,10 +57,10 @@ class PatternThumbControl extends Component { this.isAnimating = false this.addEventListeners() } - componentDidUpdate () { + componentDidUpdate() { this.updateThumbFill() } - updateThumbFill () { + updateThumbFill() { if (!this.isAnimating) { if (this.props.isFilled) { this.thumbBackground.stroke('none') @@ -71,11 +71,10 @@ class PatternThumbControl extends Component { } } else { this.thumbBackground.fill('#171717') - this.thumbBackground.stroke({ width: 1, color: '#999999', dasharray: '5,5' }) } } } - addEventListeners () { + addEventListeners() { this.thumb.on('touchstart', (e) => { e.preventDefault() this.dragStart = e.touches[0].pageX @@ -134,29 +133,8 @@ class PatternThumbControl extends Component { } }) } - onMouseMove (e) { - e.preventDefault() - let x = e.pageX - this.dragStart - if (x > containerWidth - thumbWidth) { - x = containerWidth - thumbWidth - } else if (x < 0) { - x = 0 - } - if (x > (containerWidth - thumbWidth) / 2) { - this.saveContainer.show() - this.label.hide() - this.arrowContainer.hide() - } else { - this.saveContainer.hide() - if (!this.props.isFilled) { - this.arrowContainer.show() - } else { - this.label.show() - } - } - this.thumb.x(x) - } - onMouseUp (e) { + + onMouseUp(e) { e.preventDefault() document.removeEventListener('mouseup', this.onMouseUp) document.removeEventListener('mousemove', this.onMouseMove) @@ -173,7 +151,7 @@ class PatternThumbControl extends Component { } this.thumb.x(x) } - save () { + save() { this.isAnimating = true this.thumb.animate().x(0) const _this = this @@ -194,21 +172,20 @@ class PatternThumbControl extends Component { _this.updateThumbFill() }) } - load () { + load() { this.props.loadPattern(this.props.id) } - render () { + render() { let saveStyles = { color: '#555555', position: 'absolute', right: '10px', top: '12px', zIndex: 1 } if (this.props.needsSaving && this.props.isSelected) { saveStyles.color = '#ffffff' } return ( -
    - +
    -
    +
    ) } diff --git a/src/components/play/PatternsSidebar.js b/src/components/play/PatternsSidebar.js index e95ad21..34b422f 100644 --- a/src/components/play/PatternsSidebar.js +++ b/src/components/play/PatternsSidebar.js @@ -1,12 +1,13 @@ import React, { Component } from 'react' import _ from 'lodash' -import { connect } from "react-redux"; -import PropTypes from 'prop-types'; -import { withStyles } from '@material-ui/styles'; -import Box from '@material-ui/core/Box'; +import { connect } from "react-redux" +import PropTypes from 'prop-types' +import { withStyles } from '@material-ui/styles' +import Box from '@material-ui/core/Box' +import { PRESET_LETTERS } from '../../utils/constants' import PatternThumbControl from './PatternThumbControl' -import { FirebaseContext } from '../../firebase'; +import { FirebaseContext } from '../../firebase' import { saveUserPattern, setLayerSteps, @@ -46,7 +47,7 @@ const styles = theme => ({ position: 'absolute', right: '-40px', bottom: '16px', - borderRadius: '16px', + borderRadius: 8, display: 'flex', alignItems: 'center', justifyContent: 'center', @@ -60,7 +61,7 @@ const styles = theme => ({ class PatternsSidebar extends Component { static contextType = FirebaseContext; - constructor (props) { + constructor(props) { super(props) this.state = { selectedPattern: null, @@ -71,16 +72,14 @@ class PatternsSidebar extends Component { this.onSavePattern = this.onSavePattern.bind(this) this.onMinimizeClick = this.onMinimizeClick.bind(this) } - async onLoadPattern (id) { - console.log('onLoadPattern', id); + + async onLoadPattern(id) { if (!this.props.display.isRecordingSequence) { const pattern = _.find(this.props.round.userPatterns[this.props.user.id].patterns, { id }) if (!_.isEmpty(pattern.state)) { - console.log('loading state', pattern); - console.time('loadPattern') - this.setState({ selectedPattern: pattern.id, selectedPatternNeedsSaving: false }) + // check if we have layers in the round not referenced in the pattern then set all steps in that layer to off for (const existingLayer of this.props.round.layers) { if (_.isNil(_.find(pattern.state.layers, { id: existingLayer.id })) && existingLayer.createdBy === this.props.user.id) { @@ -92,7 +91,6 @@ class PatternsSidebar extends Component { } } - // save to store first so UI updates straight away /*for (const layer of pattern.state.layers) { const layerExists = _.find(this.props.round.layers, { id: layer.id }) @@ -112,29 +110,19 @@ class PatternsSidebar extends Component { } } - //console.log('pattern.state.layers', pattern.state.layers) - _.remove(pattern.state.layers, function (n) { return layersToDelete.indexOf(n) > -1 }) - //console.log('pattern.state.layers after remove', pattern.state.layers) - // make sure layers are ordered the same - let orderedLayers = [] + // this.props.updateLayers(pattern.state.layers) for (const layer of pattern.state.layers) { let index = _.findIndex(this.props.round.layers, { id: layer.id }) - console.log('index', index); orderedLayers[index] = layer } - - //console.timeEnd('loadPattern') - - console.log('orderedLayers', orderedLayers); this.props.updateLayers(orderedLayers) - // console.log('after round update', this.props.round); // now save to firebase for (const layer of pattern.state.layers) { @@ -163,20 +151,18 @@ class PatternsSidebar extends Component { } } } - onSavePattern (id) { - //console.log('onSavePattern', id); + onSavePattern(id) { // save all steps for this user this.setState({ selectedPattern: id, selectedPatternNeedsSaving: false }) const state = this.getCurrentState(this.props.user.id) - // console.log('saving state', state); this.props.saveUserPattern(this.props.user.id, id, state) this.context.saveUserPatterns(this.props.round.id, this.props.user.id, this.props.round.userPatterns[this.props.user.id]) } - onMinimizeClick () { + onMinimizeClick() { this.setState({ isMinimized: !this.state.isMinimized }) } - getCurrentState (userId) { + getCurrentState(userId) { const userLayers = _.filter(this.props.round.layers, { createdBy: userId }) let state = {} state.layers = [] @@ -193,8 +179,9 @@ class PatternsSidebar extends Component { } return state } - render () { - const { classes } = this.props; + + render() { + const { classes, user } = this.props; let selectedPatternNeedsSaving = false; if (!_.isNil(this.state.selectedPattern) && !_.isNil(this.props.round)) { const pattern = _.find(this.props.round.userPatterns[this.props.user.id].patterns, { id: this.state.selectedPattern }) @@ -212,7 +199,8 @@ class PatternsSidebar extends Component { for (const pattern of this.props.round.userPatterns[this.props.user.id].patterns) { let item = { id: pattern.id, - label: 'P' + (pattern.order + 1), + label: PRESET_LETTERS[pattern.order], + color: user.color, userId: this.props.user.id, isFilled: !_.isEmpty(pattern.state) } @@ -225,7 +213,7 @@ class PatternsSidebar extends Component {
    {items.map((item, index) => ( - + ))}
    @@ -239,7 +227,6 @@ PatternsSidebar.propTypes = { }; const mapStateToProps = state => { - //console.log('mapStateToProps', state); return { round: state.round, user: state.user, diff --git a/src/components/play/PlayRoute.js b/src/components/play/PlayRoute.js index 43f9781..f6121ff 100644 --- a/src/components/play/PlayRoute.js +++ b/src/components/play/PlayRoute.js @@ -1,10 +1,10 @@ import React, { Component } from 'react' import PlayUI from './PlayUI' -import PatternsSidebar from './PatternsSidebar' +//import PatternsSidebar from './PatternsSidebar' import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/styles'; -import Box from '@material-ui/core/Box'; import EffectsSidebar from './EffectsSidebar'; +import Box from '@material-ui/core/Box'; import _ from 'lodash'; import Loader from 'react-loader-spinner'; import { connect } from "react-redux"; @@ -16,7 +16,6 @@ import FX from '../../audio-engine/FX' import ShareDialog from '../dialogs/ShareDialog' import { getDefaultUserBus, getDefaultUserPatterns } from '../../utils/defaultData' import LayerSettings from './layer-settings/LayerSettings'; -import OrientationDialog from '../dialogs/OrientationDialog'; import CustomSamples from '../../audio-engine/CustomSamples'; const styles = theme => ({ @@ -39,7 +38,7 @@ const styles = theme => ({ class PlayRoute extends Component { static contextType = FirebaseContext; - constructor (props) { + constructor(props) { super(props) this.isLoadingRound = false; this.hasLoadedRound = false; @@ -50,42 +49,37 @@ class PlayRoute extends Component { this.reloadCollaborationLayersThrottled = _.debounce(this.reloadCollaborationLayers, 1000) this.playUIRef = null; } - componentDidMount () { - //console.log('PlayRoute::componentDidMount()', this.props.user, this.isLoadingRound, this.hasLoadedRound, this.props.round); + componentDidMount() { this.addStartAudioContextListener() if (!this.isLoadingRound && !this.hasLoadedRound && !_.isNil(this.props.user)) { this.loadRound() } } - componentDidUpdate () { - // console.log('PlayRoute::componentDidUpdate()', this.props.user); + async componentDidUpdate() { if (!this.isLoadingRound && !this.hasLoadedRound && _.isNil(this.props.round) && !_.isNil(this.props.user)) { - this.loadRound() + await this.loadRound() } } - async componentWillUnmount () { + async componentWillUnmount() { this.isDisposing = true; this.removeFirebaseListeners() AudioEngine.stop() if (!_.isNil(this.props.round) && !_.isNil(this.props.round.currentUsers)) { this.props.setIsPlaying(false) } - /*let currentUsers = _.cloneDeep(this.props.round.currentUsers) - _.pull(currentUsers, this.props.user.id) - await this.context.updateRound(this.props.round.id, { currentUsers })*/ this.props.setRound(null) this.props.setUsers([]) this.isLoadingRound = false; this.hasLoadedRound = false; } - async loadRound () { + async loadRound() { this.isLoadingRound = true; let roundId = this.props.location.pathname.split('/play/')[1] - // console.log('PlayRoute::loadRound()', roundId); let round = await this.context.getRound(roundId) + console.log('got round', round) if (_.isNil(round) || _.isNil(round.currentUsers)) { // probably deleted round this.props.history.push('/rounds') @@ -116,15 +110,16 @@ class PlayRoute extends Component { } // load audio + console.log('loading custom samples ') CustomSamples.init(this.context) await AudioEngine.init() Instruments.init() FX.init() - //console.log('PlayRoute loading audio engine'); + console.log('audio engine loading round') await AudioEngine.load(round) - // console.log('PlayRoute finished loading audio engine'); this.props.setUsers(currentUsers) + console.log('setting round') this.props.setRound(round) this.hasLoadedRound = true this.isLoadingRound = false @@ -133,13 +128,12 @@ class PlayRoute extends Component { this.addUsersListeners() } - addFirebaseListeners () { + addFirebaseListeners() { const _this = this // Round this.context.db.collection('rounds').doc(this.props.round.id).onSnapshot(async (doc) => { const updatedRound = doc.data() - // console.log('### round change listener fired', this.props.round, updatedRound); if (_.isNull(this.props.round)) { // probably deleted round _this.props.history.push('/rounds') @@ -147,26 +141,15 @@ class PlayRoute extends Component { } if (!this.isDisposing) { if (!_.isEqual(_this.props.round.currentUsers, updatedRound.currentUsers)) { - // console.log('new user added or removed'); let users = [] for (const userId of updatedRound.currentUsers) { let user = await _this.context.loadUser(userId) users.push(user) } - // console.log('setUsers()', users); _this.props.setUsers(users) _this.props.setRoundCurrentUsers(updatedRound.currentUsers) _this.addUsersListeners() } - if (!_.isEqual(_this.props.round.isPlaying, updatedRound.isPlaying)) { - if (updatedRound.isPlaying) { - AudioEngine.play() - _this.props.setIsPlaying(true) - } else { - AudioEngine.stop() - _this.props.setIsPlaying(false) - } - } if (!_.isEqual(_this.props.round.bpm, updatedRound.bpm)) { AudioEngine.setTempo(updatedRound.bpm) _this.props.setRoundBpm(updatedRound.bpm) @@ -180,23 +163,14 @@ class PlayRoute extends Component { // Layers this.layersChangeListenerUnsubscribe = this.context.db.collection('rounds').doc(this.props.round.id).collection('layers').onSnapshot((layerCollectionSnapshot) => { - // console.log('### layer change listener fired'); layerCollectionSnapshot.docChanges().forEach(change => { if (change.type === 'modified') { - // console.log('Modified layer: ', change.doc.data()); const layer = change.doc.data() if (layer.createdBy !== _this.props.user.id) { _this.reloadCollaborationLayersThrottled() - } else { - // console.log('ignoring own firebase change'); } } - if (change.type === 'added') { - // console.log('New layer: ', change.doc.data()); - _this.reloadCollaborationLayersThrottled() - } - if (change.type === 'removed') { - // console.log('Removed layer: ', change.doc.data()); + if (change.type === 'added' || change.type === 'removed') { _this.reloadCollaborationLayersThrottled() } }); @@ -204,12 +178,10 @@ class PlayRoute extends Component { // Userbus (FX) this.userBusChangeListenerUnsubscribe = this.context.db.collection('rounds').doc(this.props.round.id).collection('userBuses').onSnapshot((userBusesCollectionSnapshot) => { - // console.log('### userbus change listener fired'); userBusesCollectionSnapshot.docChanges().forEach(change => { const userBus = change.doc.data() userBus.id = change.doc.id if (change.type === 'modified') { - // console.log('Modified userbus: ', change.doc.data(), _this.props.round.userBuses[userBus.id]); _this.handleUserBusChange(userBus) } if (change.type === 'added') { @@ -218,36 +190,39 @@ class PlayRoute extends Component { AudioEngine.addUser(userBus.id, userBus.fx) } } - if (change.type === 'removed') { - //console.log('Removed userbus: ', change.doc.data()); - } }); }) // UserPatterns this.userPatternsChangeListenerUnsubscribe = this.context.db.collection('rounds').doc(this.props.round.id).collection('userPatterns').onSnapshot((userPatternsCollectionSnapshot) => { - // console.log('### layer change listener fired'); - userPatternsCollectionSnapshot.docChanges().forEach(change => { + userPatternsCollectionSnapshot.docChanges().forEach(async change => { + const data = change.doc.data(); + const userId = change.doc.id; + const newUser = await _this.context.loadUser(userId) + const newUsers = _.cloneDeep(this.props.users) if (change.type === 'modified') { - // console.log('Modified layer: ', change.doc.data()); const userPatterns = change.doc.data() userPatterns.id = change.doc.id _this.handleUserPatternsChange(userPatterns) } if (change.type === 'added') { - // console.log('New layer: ', change.doc.data()); - // _this.reloadCollaborationLayersThrottled() - } - if (change.type === 'removed') { - // console.log('Removed layer: ', change.doc.data()); - // _this.reloadCollaborationLayersThrottled() + let newRound = _.cloneDeep(this.props.round) + let userPatterns = newRound.userPatterns + if (_.isNil(userPatterns[userId]) && this.props.user.id !== userId) { + /** Feels like all these should be implemented elsewhere */ + newRound.userPatterns[userId] = data + newRound.currentUsers.push(userId) + newUsers.push(newUser) + _this.props.setUsers(newUsers) + _this.props.setRoundCurrentUsers(newRound.currentUsers) + _this.props.setRound(newRound) + } } }); }) } - removeFirebaseListeners () { - //console.log('removeFirebaseListeners()'); + removeFirebaseListeners() { if (!_.isNil(this.layersChangeListenerUnsubscribe)) { this.layersChangeListenerUnsubscribe(); } @@ -257,31 +232,29 @@ class PlayRoute extends Component { this.removeUsersListeners() } - addUsersListeners () { + addUsersListeners() { this.removeUsersListeners() this.usersChangeListenersUnsubscribe = [] const _this = this; for (const user of this.props.users) { let userListenerUnsubscribe = this.context.db.collection('users').doc(user.id).onSnapshot((doc) => { - // console.log('### user change listener fired'); _this.loadUsers() }) this.usersChangeListenersUnsubscribe.push(userListenerUnsubscribe) } } - async loadUsers () { + async loadUsers() { let users = [] for (const userId of this.props.round.currentUsers) { let user = await this.context.loadUser(userId) users.push(user) } - // console.log('setUsers()', users); this.props.setUsers(users) this.props.setRoundCurrentUsers(this.props.round.currentUsers) } - removeUsersListeners () { + removeUsersListeners() { if (!_.isNil(this.usersChangeListenersUnsubscribe)) { for (const unsubscribe of this.usersChangeListenersUnsubscribe) { unsubscribe() @@ -289,12 +262,11 @@ class PlayRoute extends Component { } } - handleUserBusChange (userBus) { + handleUserBusChange(userBus) { let fxOrderChanged = false for (let fx of userBus.fx) { const currentFx = _.find(this.props.round.userBuses[userBus.id].fx, { id: fx.id }) if (!_.isEqual(fx.isOverride, currentFx.isOverride)) { - // console.log('found fx override change', fx, currentFx); AudioEngine.busesByUser[userBus.id].fx[fx.id].override = fx.isOverride this.props.setUserBusFxOverride(userBus.id, fx.id, fx.isOverride) } @@ -307,15 +279,13 @@ class PlayRoute extends Component { } } - handleUserPatternsChange (userPatterns) { - console.log('userPatternsChange', userPatterns); + handleUserPatternsChange(userPatterns) { this.props.setIsPlayingSequence(userPatterns.id, userPatterns.isPlayingSequence) } // if any of the subcollections for a collaboration user change, trigger a (throttled) reload of all collaboration layers as there could be multiple changes // to do: maybe add an id to the query to make sure we don't overwrite the local round with an await result that comes in late - async reloadCollaborationLayers () { - //console.log('reloadCollaborationLayers()'); + async reloadCollaborationLayers() { const _this = this; if (!_.isNil(this.props.round)) { const newRound = await this.context.getRound(this.props.round.id) @@ -325,7 +295,6 @@ class PlayRoute extends Component { const oldLayers = _.filter(this.props.round.layers, (layer) => { return layer.createdBy !== _this.props.user.id }) - // console.log('comparing layers', _.isEqual(newLayers, oldLayers)); if (!_.isEqual(newLayers, oldLayers)) { const userLayers = _.filter(this.props.round.layers, (layer) => { return layer.createdBy === _this.props.user.id @@ -339,24 +308,22 @@ class PlayRoute extends Component { } // user needs to click something in order to start audio context, if they're a collaborator then they may not click play so use the first click to start audio context - addStartAudioContextListener () { + addStartAudioContextListener() { window.addEventListener('touchstart', this.startAudioContext) } - startAudioContext () { - //console.log('startAudioContext()'); + startAudioContext() { AudioEngine.startAudioContext() this.removeStartAudioContextListener() } - removeStartAudioContextListener () { + removeStartAudioContextListener() { window.removeEventListener('touchstart', this.startAudioContext) } - adjustLayerTimingInstant (id, percent) { + adjustLayerTimingInstant(id, percent) { this.playUIRef.adjustLayerTiming(id, percent) } - render () { - // console.log('PlayRoute::render()', this.props.round); + render() { const { classes, round } = this.props; return ( @@ -375,11 +342,11 @@ class PlayRoute extends Component { visible={true} /> } - - - + + + ) } diff --git a/src/components/play/PlayUI.js b/src/components/play/PlayUI.js index 695a73e..f50008b 100644 --- a/src/components/play/PlayUI.js +++ b/src/components/play/PlayUI.js @@ -1,23 +1,43 @@ import React, { Component } from 'react'; -import * as _ from 'lodash'; +import _ from 'lodash'; import { SVG } from '@svgdotjs/svg.js' import '@svgdotjs/svg.panzoom.js' -import { HTML_UI_Params, KEY_MAPPINGS } from '../../utils/constants' +import { HTML_UI_Params, PRESET_LETTERS } from '../../utils/constants' import { connect } from "react-redux"; import AudioEngine from '../../audio-engine/AudioEngine' import { getDefaultLayerData } from '../../utils/defaultData'; -import { TOGGLE_STEP, ADD_LAYER, SET_SELECTED_LAYER_ID, SET_IS_SHOWING_LAYER_SETTINGS, SET_IS_PLAYING, UPDATE_STEP, SET_IS_SHOWING_ORIENTATION_DIALOG, UPDATE_LAYERS, SET_CURRENT_SEQUENCE_PATTERN } from '../../redux/actionTypes' +import { SET_LAYER_MUTE, TOGGLE_STEP, ADD_LAYER, SET_SELECTED_LAYER_ID, SET_IS_SHOWING_LAYER_SETTINGS, UPDATE_STEP, SET_IS_SHOWING_ORIENTATION_DIALOG, UPDATE_LAYERS, SET_CURRENT_SEQUENCE_PATTERN } from '../../redux/actionTypes' import { FirebaseContext } from '../../firebase/' import * as Tone from 'tone'; import { withStyles } from '@material-ui/styles'; import PropTypes from 'prop-types'; import { numberRange } from '../../utils/index' import Instruments from '../../audio-engine/Instruments' +import { getDefaultUserPatternSequence } from '../../utils/defaultData' import { detailedDiff } from 'deep-object-diff'; +import { + setIsPlaying, + setIsRecordingSequence, + setUserPatternSequence, + updateLayers, + setIsPlayingSequence, + setCurrentSequencePattern, + saveUserPattern +} from "../../redux/actions"; const styles = theme => ({ button: { cursor: 'pointer' }, + fadeIn: { + transition: "all 0.5s ease-in" + }, + fadeOut: { + transition: "all 0.5s ease-out" + }, + smallCross: { + width: 5, + height: 5 + }, buttonIcon: { pointerEvents: 'none' } @@ -25,17 +45,33 @@ const styles = theme => ({ class PlayUI extends Component { static contextType = FirebaseContext - constructor (props) { + constructor(props) { super(props) + this.state = { + selectedPattern: null, + isMinimized: false + } + this.selectedPatternNeedsSaving = false this.isZooming = false this.isPanning = false + this.isRecordingSequence = false + this.isPlayingSequence = false this.stepGraphics = [] + this.microStepGraphics = [] this.layerGraphics = [] - this.round = null; // local copy of round, prevent mutating store. + this.microLayerGraphics = [] + this.microPatternGraphics = [] + this.sequenceGraphics = [] + this.sequencerButtons = [] + this.activePattern = undefined + this.activeSequence = undefined + this.round = null // local copy of round, prevent mutating store. this.isOn = false this.editAllLayers = false this.swipeToggleActive = false this.userColors = {}; + this.isScrolling = false; + this.stepOnTimer = 0; this.onWindowResizeThrottled = _.throttle(this.onWindowResize.bind(this), 1000) this.selectedLayerId = null; this.onKeypress = this.onKeypress.bind(this) @@ -44,18 +80,25 @@ class PlayUI extends Component { this.sequencerParts = {} } - async componentDidMount () { + async componentDidMount() { + const { round, user } = this.props // register this component with parent so we can do some instant updates bypassing redux for speed this.props.childRef(this) - - this.createRound() + this.isPlayingSequence = round.userPatterns[user.id].isPlayingSequence + window.addEventListener('click', this.interfaceClicked) window.addEventListener('resize', this.onWindowResizeThrottled) window.addEventListener('keypress', this.onKeypress) this.addBackgroundEventListeners() this.checkOrientation() + // load sequence if enabled + const patterns = round.userPatterns + this.loadSequence(patterns) + this.setDefaultPattern() + await this.createRound() } - async componentWillUnmount () { + async componentWillUnmount() { + window.removeEventListener('click', this.interfaceClicked) window.removeEventListener('resize', this.onWindowResizeThrottled) window.removeEventListener('keypress', this.onKeypress) this.removeBackgroundEventListeners() @@ -63,8 +106,21 @@ class PlayUI extends Component { this.disposeToneEvents() } - createRound () { - // console.log('createRound()'); + setDefaultPattern = async () => { + const { user, round } = this.props + const defaultPattern = round.userPatterns[user.id].patterns[0] + this.activePatternId = defaultPattern.id + this.onLoadPattern(defaultPattern.id) + } + + interfaceClicked = (e) => { + if (!this.selectedLayerId && this.props.selectedLayer) { + this.props.dispatch({ type: SET_SELECTED_LAYER_ID, payload: { layerId: null } }) + this.props.dispatch({ type: SET_IS_SHOWING_LAYER_SETTINGS, payload: { value: false } }) + } + } + + async createRound() { this.round = _.cloneDeep(this.props.round) this.userColors = this.getUserColors() // Create SVG container @@ -78,7 +134,6 @@ class PlayUI extends Component { .size(this.containerWidth, this.containerHeight) .panZoom({ zoomMin: 0.2, zoomMax: 1.3, zoomFactor: 0.2 }) this.container.on('panning', (e) => { - // console.log('round panning'); if (this.stepIsPanning) { e.preventDefault() } @@ -87,9 +142,32 @@ class PlayUI extends Component { this.draw() } - async componentDidUpdate () { - console.log('componentDidUpdate()', this.round, this.props.round) - console.time('componentDidUpdate') + async componentDidUpdate(prevProps) { + const { round, user, display, setIsRecordingSequence } = this.props + const oldRound = prevProps.round + let redraw = false + let shouldRecalculateParts = false + const _this = this + const sameLayerLength = prevProps.round.layers.length === round.layers.length + + this.isPlayingSequence = round.userPatterns[user.id].isPlayingSequence + + !this.activePatternId && + this.setDefaultPattern() + + let diff = detailedDiff(this.round, this.props.round) + if (!_.isEqual(round.userPatterns[user.id].isPlayingSequence, oldRound.userPatterns[user.id].isPlayingSequence)) { + redraw = true + } + + if (!_.isEqual(display.isRecordingSequence, prevProps.display.isRecordingSequence)) { + redraw = true + } + + if (!_.isEqual(this.isRecordingSequence, display.isRecordingSequence)) { + /** update props to match state */ + setIsRecordingSequence(this.isRecordingSequence) + } // whole round has changed if (this.round.id !== this.props.round.id) { @@ -99,14 +177,11 @@ class PlayUI extends Component { return } - let diff = detailedDiff(this.round, this.props.round) - console.log('diff', diff); - - let redraw = false - let shouldRecalculateParts = false - const _this = this + if (!sameLayerLength) { + await this.onSavePattern(this.activePatternId) + } - // remove layer + //layer removal for (let layer of this.round.layers) { let newLayer = _.find(this.props.round.layers, { id: layer.id }) if (_.isNil(newLayer)) { @@ -117,26 +192,11 @@ class PlayUI extends Component { // sequence update if (!_.isNil(diff.updated.userPatterns)) { - for (let [userPatternsId, userPatterns] of Object.entries(diff.updated.userPatterns)) { - if (!_.isNil(userPatterns.isPlayingSequence)) { - if (userPatterns.isPlayingSequence) { - console.log('isPlayingSequence turned on', userPatterns, this.props.round.userPatterns[userPatternsId]); - const newUserPatterns = this.props.round.userPatterns[userPatternsId] - this.startSequence(newUserPatterns) - } else { - console.log('isPlayingSequence turned off'); - this.stopSequence(userPatternsId) - } - } - } - } - - // step updates - if (!_.isNil(diff.updated.layers)) { - shouldRecalculateParts = true + this.loadSequence(diff.updated.userPatterns) redraw = true } + // tempo changed if (this.round.bpm !== this.props.round.bpm) { this.round.bpm = this.props.round.bpm AudioEngine.setTempo(this.round.bpm) @@ -154,30 +214,26 @@ class PlayUI extends Component { // add layer if (!_.isNil(diff.added.layers)) { for (let [, layer] of Object.entries(diff.added.layers)) { - await AudioEngine.createTrack(layer) - redraw = true + AudioEngine.createTrack(layer) } + shouldRecalculateParts = true + redraw = true } - - // Check for layer type or instrument changes for (let layer of this.round.layers) { let newLayer = _.find(this.props.round.layers, { id: layer.id }) if (!_.isNil(newLayer) && !_.isEqual(layer.instrument, newLayer.instrument)) { // instrument has changed - // console.log('instrument has changed', newLayer.instrument); AudioEngine.tracksById[newLayer.id].setInstrument(newLayer.instrument) this.updateLayerLabelText(layer.id, newLayer.instrument.sampler) } if (!_.isNil(newLayer) && !_.isEqual(layer.type, newLayer.type)) { // type has changed - //console.log('layer type has changed'); AudioEngine.tracksById[newLayer.id].setType(newLayer.type, newLayer.automationFxId) } if (!_.isNil(newLayer) && !_.isEqual(layer.automationFxId, newLayer.automationFxId)) { // automation has changed - // console.log('layer automation fx id has changed'); AudioEngine.tracksById[newLayer.id].setAutomatedFx(newLayer.automationFxId) } } @@ -185,7 +241,6 @@ class PlayUI extends Component { for (let layer of this.round.layers) { let newLayer = _.find(this.props.round.layers, { id: layer.id }) if (!_.isNil(newLayer) && !_.isEqual(layer.gain, newLayer.gain)) { - // console.log('gain has changed', newLayer.gain) AudioEngine.tracksById[newLayer.id].setVolume(newLayer.gain) } } @@ -194,8 +249,8 @@ class PlayUI extends Component { for (let layer of this.round.layers) { let newLayer = _.find(this.props.round.layers, { id: layer.id }) if (!_.isNil(newLayer) && !_.isEqual(layer.isMuted, newLayer.isMuted)) { - // console.log('mute has changed', newLayer.isMuted) - AudioEngine.tracksById[newLayer.id].setMute(newLayer.isMuted) + AudioEngine.tracksById[newLayer.id]?.setMute(newLayer.isMuted) + redraw = true } } @@ -212,10 +267,6 @@ class PlayUI extends Component { } } - - - - if (shouldRecalculateParts) { AudioEngine.recalculateParts(this.props.round) } @@ -223,211 +274,34 @@ class PlayUI extends Component { this.clear() this.round = _.cloneDeep(this.props.round) _this.draw(false) - } else { - this.round = _.cloneDeep(this.props.round) } + this.round = _.cloneDeep(this.props.round) + } - console.timeEnd('componentDidUpdate') - /* - - if (this.round.id !== this.props.round.id) { - // whole round has changed - this.round = _.cloneDeep(this.props.round) - AudioEngine.load(this.props.round) - this.draw() - return - } - - if (this.round.bpm !== this.props.round.bpm) { - this.round.bpm = this.props.round.bpm - AudioEngine.setTempo(this.round.bpm) - this.reclaculateIndicatorAnimation() - this.adjustAllLayerOffsets() - } - - // User profile color changed - const userColors = this.getUserColors() - if (!_.isEqual(userColors, this.userColors)) { - this.userColors = userColors - redraw = true - } - - // Edit all interactions changed - if (this.editAllLayers !== this.props.editAllLayers) { - this.editAllLayers = this.props.editAllLayers - this.removeAllStepEventListeners() - for (let layerGraphic of this.layerGraphics) { - if (this.editAllLayers) { - layerGraphic.isAllowedInteraction = true - } else { - // console.log('layer', _.find(this.props.round.layers, { id: layerGraphic.id })); - const layer = _.find(this.props.round.layers, { id: layerGraphic.id }) - if (!_.isNil(layer)) { - layerGraphic.isAllowedInteraction = layer.createdBy === this.props.user.id - } - } - this.addLayerEventListeners(layerGraphic) - } - for (let stepGraphic of this.stepGraphics) { - if (this.editAllLayers) { - stepGraphic.isAllowedInteraction = true - } else { - stepGraphic.isAllowedInteraction = this.stepLayerDictionary[stepGraphic.id].createdBy === this.props.user.id - } - this.addStepEventListeners(stepGraphic) - } - } - - if (!this.isOn && this.props.isOn && !_.isNil(this.positionLine)) { - //console.log('playing timeline'); - // adding 200ms delay to compensate for starting audio with delay to reduce audio glitches. Todo: sync this better with the transport - _.delay(() => { - this.positionLine.timeline().play() - this.isOn = true - }, 200) - } else if (this.isOn && !this.props.isOn && !_.isNil(this.positionLine)) { - //console.log('pausing timeline'); - _.delay(() => { - this.positionLine.timeline().stop() - this.isOn = false - }, 200) - } - - // check for one or more layers added - this.cacheStepLayers() - for (let layer of this.props.round.layers) { - let oldLayer = _.find(this.round.layers, { id: layer.id }) - if (_.isNil(oldLayer)) { - await AudioEngine.createTrack(layer) - redraw = true - } - } - - for (let layer of this.round.layers) { - let newLayer = _.find(this.props.round.layers, { id: layer.id }) - if (_.isNil(newLayer)) { - AudioEngine.removeTrack(layer.id) - redraw = true - } - } - - // check for number of steps per layer changed - let previousSteps = [] - for (let i = 0; i < this.round.layers.length; i++) { - const layer = this.round.layers[i] - previousSteps.push(...layer.steps) - const newLayer = _.find(this.props.round.layers, { id: layer.id }) - if (!_.isNil(newLayer)) { - if (newLayer.steps.length !== layer.steps.length) { - // number of steps has changed - redraw = true - AudioEngine.recalculateParts(this.props.round) - } - } - } - - // Check if an individual step has changed - let newSteps = [] - for (let layer of this.props.round.layers) { - for (let newStep of layer.steps) { - newSteps.push(newStep) - } - - } - let shouldRecalculateParts = false - // console.timeEnd('componentDidUpdate B3 B') - for (let previousStep of previousSteps) { - // console.time('componentDidUpdate B3 C') - let newStep = _.find(newSteps, { id: previousStep.id }) - // console.timeEnd('componentDidUpdate B3 C') - if (!_.isNil(newStep)) { - // console.time('componentDidUpdate B3 D') - //const shouldUpdate = !_.isEqual(previousStep, newStep) - let shouldUpdate = false - if (previousStep.isOn != newStep.isOn) { - shouldUpdate = true - } - // console.timeEnd('componentDidUpdate B3 D') - if (shouldUpdate) { - // console.log('found changed step', previousStep, newStep); - // console.time('componentDidUpdate B3 E') - //this.updateStep(newStep, true) - // console.timeEnd('componentDidUpdate B3 E') - // console.time('componentDidUpdate B3 F') - shouldRecalculateParts = true - //AudioEngine.recalculateParts(this.props.round) - // console.timeEnd('componentDidUpdate B3 F') - } - } - } - - - // Check for layer type or instrument changes - for (let layer of this.round.layers) { - let newLayer = _.find(this.props.round.layers, { id: layer.id }) - if (!_.isNil(newLayer) && !_.isEqual(layer.instrument, newLayer.instrument)) { - // instrument has changed - // console.log('instrument has changed', newLayer.instrument); - AudioEngine.tracksById[newLayer.id].setInstrument(newLayer.instrument) - this.updateLayerLabelText(layer.id, newLayer.instrument.sampler) - } - if (!_.isNil(newLayer) && !_.isEqual(layer.type, newLayer.type)) { - // type has changed - //console.log('layer type has changed'); - AudioEngine.tracksById[newLayer.id].setType(newLayer.type, newLayer.automationFxId) - } - if (!_.isNil(newLayer) && !_.isEqual(layer.automationFxId, newLayer.automationFxId)) { - // automation has changed - // console.log('layer automation fx id has changed'); - AudioEngine.tracksById[newLayer.id].setAutomatedFx(newLayer.automationFxId) - } - } - // Check for gain changes - for (let layer of this.round.layers) { - let newLayer = _.find(this.props.round.layers, { id: layer.id }) - if (!_.isNil(newLayer) && !_.isEqual(layer.gain, newLayer.gain)) { - // console.log('gain has changed', newLayer.gain) - AudioEngine.tracksById[newLayer.id].setVolume(newLayer.gain) - } - } - - // Check for mute changes - for (let layer of this.round.layers) { - let newLayer = _.find(this.props.round.layers, { id: layer.id }) - if (!_.isNil(newLayer) && !_.isEqual(layer.isMuted, newLayer.isMuted)) { - // console.log('mute has changed', newLayer.isMuted) - AudioEngine.tracksById[newLayer.id].setMute(newLayer.isMuted) - } - } - - // Check for layer time offset changes - for (let layer of this.round.layers) { - let newLayer = _.find(this.props.round.layers, { id: layer.id }) - if (!_.isNil(newLayer) && !_.isEqual(layer.timeOffset, newLayer.timeOffset)) { - AudioEngine.recalculateParts(this.props.round) - this.adjustLayerOffset(newLayer.id, newLayer.percentOffset, newLayer.timeOffset) - } - if (!_.isNil(newLayer) && !_.isEqual(layer.percentOffset, newLayer.percentOffset)) { - AudioEngine.recalculateParts(this.props.round) - this.adjustLayerOffset(newLayer.id, newLayer.percentOffset, newLayer.timeOffset) - } - } - - // Check for sequence changes - for (let [, userPatterns] of Object.entries(this.round.userPatterns)) { - let newUserPatterns = _.find(this.props.round.userPatterns, { id: userPatterns.id }) - if (!userPatterns.isPlayingSequence && newUserPatterns.isPlayingSequence) { - //console.log('isPlayingSequence turned on'); - AudioEngine.recalculateParts(this.props.round) - this.calculateSequence(newUserPatterns) + loadSequence = (patterns) => { + const { round } = this.props + for (let [userPatternsId, userPatterns] of Object.entries(patterns)) { + if (!_.isNil(userPatterns.isPlayingSequence)) { + if (userPatterns.isPlayingSequence) { + const newUserPatterns = round.userPatterns[userPatternsId] + this.startSequence(newUserPatterns) } else { - // console.log('isPlayingSequence turned off'); + this.stopSequence(userPatternsId) } } - */ + } + } + + onMuteToggle = (props) => { + const isMuted = props.selectedLayer?.isMuted + if (props.selectedLayer) { + AudioEngine.tracksById[props.selectedLayer.id]?.setMute(!isMuted) + props.dispatch({ type: SET_LAYER_MUTE, payload: { id: props.selectedLayer.id, value: !isMuted, user: props.user.id } }) + this.context.updateLayer(props.round.id, props.selectedLayer.id, { isMuted: !isMuted }) + } } - getStep (id) { + getStep(id) { let steps = [] for (let layer of this.round.layers) { steps.push(...layer.steps) @@ -435,30 +309,14 @@ class PlayUI extends Component { return _.find(steps, { id }) } - draw (shouldAnimate) { - // console.log('draw()', this.containerWidth, this.containerheight); + async draw(shouldAnimate) { + const { isPlaying } = this.props + this.clear() - const _this = this this.orderLayers() this.cacheStepLayers() - - - // position line - /* this.isOn = this.props.isOn - const positionLineLength = (HTML_UI_Params.addNewLayerButtonDiameter / 2) + (HTML_UI_Params.initialLayerPadding / 2) + ((HTML_UI_Params.stepDiameter + HTML_UI_Params.layerPadding) * this.round.layers.length) - - const positionLineWidth = 16 - const positionLineTime = (60 / this.round.bpm) * 4000 - this.positionLine = this.container.rect(positionLineWidth, positionLineLength).fill('#666666') - this.positionLine.move((this.containerWidth / 2) - (positionLineWidth / 2), (this.containerHeight / 2) - positionLineLength) - this.positionLineAnimation = this.positionLine.animate({ duration: positionLineTime }).ease('-').transform({ rotate: 360, relative: true, origin: 'bottom center' }).loop() - if (!this.isOn) { - this.positionLine.timeline().pause() - } else { - this.positionLine.timeline().seek(AudioEngine.getPositionMilliseconds()) - }*/ // draw layers this.stepGraphics = [] this.layerGraphics = [] @@ -472,19 +330,25 @@ class PlayUI extends Component { this.activityIndicator = this.container.circle(HTML_UI_Params.activityIndicatorDiameter).fill({ color: '#fff', opacity: 0 }) // add layer button - this.addLayerButton = this.container.circle(HTML_UI_Params.addNewLayerButtonDiameter).attr({ fill: '#1B1B1B' }).stroke({ width: 1, color: this.userColors[this.props.user.id], dasharray: '5,5' }) - this.addLayerButton.x((this.containerWidth / 2) - (HTML_UI_Params.addNewLayerButtonDiameter / 2)) - this.addLayerButton.y((this.containerHeight / 2) - (HTML_UI_Params.addNewLayerButtonDiameter / 2)) - this.addLayerButton.click(() => { - _this.onAddLayerClick() - }) - this.addLayerButton.addClass(this.props.classes.button) - //this.addLayerButton.svg('') - this.addLayerButtonIcon = this.container.nested() - this.addLayerButtonIcon.svg('') - this.addLayerButtonIcon.x((this.containerWidth / 2) - 24) - this.addLayerButtonIcon.y((this.containerHeight / 2) - 25) - this.addLayerButtonIcon.addClass(this.props.classes.buttonIcon) + this.playbackToggle = this.container.circle(HTML_UI_Params.addNewLayerButtonDiameter).stroke({ width: 1, color: 'rgba(0,0,0,0)' }).fill('white').opacity('0.1') + this.playbackToggle.x((this.containerWidth / 2) - (HTML_UI_Params.addNewLayerButtonDiameter / 2)) + this.playbackToggle.y((this.containerHeight / 2) - (HTML_UI_Params.addNewLayerButtonDiameter / 2)) + this.playbackToggle.click(this.onPlaybackToggle) + this.playbackToggle.addClass(this.props.classes.button) + this.playbackToggleIcon = this.container.nested() + if (!isPlaying) + this.playbackToggleIcon.svg(` + `) + if (isPlaying) + this.playbackToggleIcon.svg(` + + + `) + this.playbackToggleIcon.x((this.containerWidth / 2) - 17.5) + this.playbackToggleIcon.y((this.containerHeight / 2) - 19.5) + this.playbackToggleIcon.addClass(this.props.classes.buttonIcon) this.stepModal = this.container.nested() this.stepModalBackground = this.stepModal.rect(HTML_UI_Params.stepModalDimensions, HTML_UI_Params.stepModalDimensions).fill({ color: '#000', opacity: 0.8 }).radius(HTML_UI_Params.stepModalThumbDiameter / 2) @@ -521,14 +385,16 @@ class PlayUI extends Component { } this.scheduleToneEvents() + await this.renderPatternPresetsSequencer(); } - scheduleToneEvents () { + scheduleToneEvents() { this.disposeToneEvents() const _this = this this.toneParts = [] for (const layer of this.round.layers) { - const notes = this.convertStepsToNotes(layer.steps, this.userColors[layer.createdBy]) + const color = layer.isMuted ? '#FFFFFF' : this.userColors[layer.createdBy] + const notes = this.convertStepsToNotes(layer.steps, color) for (let note of notes) { note.time += 'i'; } @@ -536,10 +402,9 @@ class PlayUI extends Component { Tone.Draw.schedule(function () { const stepGraphic = _.find(_this.stepGraphics, { id: note.id }) if (!_.isNil(stepGraphic)) { - stepGraphic.stroke({ color: '#FFFFFF' }) - stepGraphic.animate().stroke({ color: note.color }) + stepGraphic.stroke({ color: '#FFFFFF', opacity: layer.isMuted ? 0.1 : 1 }) + stepGraphic.animate().stroke({ color: note.color, opacity: layer.isMuted ? 0.1 : 1 }) } - }, time) }, notes) part.loop = true @@ -549,7 +414,7 @@ class PlayUI extends Component { } } - disposeToneEvents () { + disposeToneEvents() { if (!_.isNil(this.toneParts)) { for (let part of this.toneParts) { if (!_.isNil(part) && !_.isNil(part._events)) { @@ -559,7 +424,7 @@ class PlayUI extends Component { } } - convertStepsToNotes (steps, userColor) { + convertStepsToNotes(steps, userColor) { const PPQ = Tone.Transport.PPQ const totalTicks = PPQ * 4 const ticksPerStep = Math.round(totalTicks / steps.length) @@ -577,8 +442,7 @@ class PlayUI extends Component { return notes } - startSequence (userPatterns) { - console.log('calculating seq', userPatterns); + startSequence(userPatterns) { const PPQ = Tone.Transport.PPQ const ticksPerBar = PPQ * 4 const notes = [] @@ -604,10 +468,7 @@ class PlayUI extends Component { } note.time += 'i' } - // console.log('loading seq part', notes); - // const shouldUpdateGraphics = userPatterns.id === this.props.user.id let part = new Tone.Part(function (time, note) { - //console.log('seq note', note); _this.loadPatternPriority(userPatterns.id, note.id, note.order) // if (shouldUpdateGraphics) { Tone.Draw.schedule(function () { @@ -621,15 +482,13 @@ class PlayUI extends Component { this.sequencerParts[userPatterns.id] = part } - stopSequence (id) { + stopSequence(id) { if (!_.isNil(this.sequencerParts[id])) { this.sequencerParts[id].stop() } } - loadPatternPriority (userId, id, order) { - // console.log('load pattern', id); - // console.time('loadPatternPriority') + async loadPatternPriority(userId, id, order) { //this.props.dispatch({ type: SET_CURRENT_SEQUENCE_PATTERN, payload: { value: order } }) const pattern = _.find(this.props.round.userPatterns[userId].patterns, { id }) if (!_.isEmpty(pattern.state)) { @@ -656,9 +515,6 @@ class PlayUI extends Component { _.remove(pattern.state.layers, function (n) { return layersToDelete.indexOf(n) > -1 }) - //this.props.updateLayers(pattern.state.layers) - - // console.log('loadPattern updating internal layers', this.round.layers, pattern.state.layers); for (let layer of this.round.layers) { let patternLayer = _.find(pattern.state.layers, { id: layer.id }) @@ -668,66 +524,17 @@ class PlayUI extends Component { } AudioEngine.recalculateParts(this.round) - // console.timeEnd('loadPattern') - - // this.props.dispatch({ type: UPDATE_LAYERS, payload: { layers: pattern.state.layers } }) - // this.props.dispatch({ type: SET_CURRENT_SEQUENCE_PATTERN, payload: { value: order } }) } - } - loadPattern (userId, id, order) { - // console.log('load pattern', id); - // console.time('loadPattern') - //this.props.dispatch({ type: SET_CURRENT_SEQUENCE_PATTERN, payload: { value: order } }) - const pattern = _.find(this.props.round.userPatterns[userId].patterns, { id }) - /*if (!_.isEmpty(pattern.state)) { - // check if we have layers in the round not referenced in the pattern then set all steps in that layer to off - for (const existingLayer of this.props.round.layers) { - if (_.isNil(_.find(pattern.state.layers, { id: existingLayer.id })) && existingLayer.createdBy === this.props.user.id) { - let existingLayerClone = _.cloneDeep(existingLayer) - for (const step of existingLayerClone.steps) { - step.isOn = false - } - pattern.state.layers.push(existingLayerClone) - } - } - - // check we haven't deleted the layer that is referenced in the pattern - let layersToDelete = [] - for (const layer of pattern.state.layers) { - const layerExists = _.find(this.props.round.layers, { id: layer.id }) - if (_.isNil(layerExists)) { - layersToDelete.push(layer) - } - } - - _.remove(pattern.state.layers, function (n) { - return layersToDelete.indexOf(n) > -1 - }) - //this.props.updateLayers(pattern.state.layers) - - // console.log('loadPattern updating internal layers', this.round.layers, pattern.state.layers); - - // for (let layer of this.round.layers) { - // layer.steps = _.find(pattern.state.layers, { id: layer.id }).steps - // } - - //AudioEngine.recalculateParts(this.round) - // console.timeEnd('loadPattern') - - this.props.dispatch({ type: UPDATE_LAYERS, payload: { layers: pattern.state.layers } }) - this.props.dispatch({ type: SET_CURRENT_SEQUENCE_PATTERN, payload: { value: order } }) - }*/ - - - this.props.dispatch({ type: UPDATE_LAYERS, payload: { layers: this.round.layers } }) + loadPattern(userId, id, order) { + this.round.layers && this.props.dispatch({ type: UPDATE_LAYERS, payload: { layers: this.round.layers } }) this.props.dispatch({ type: SET_CURRENT_SEQUENCE_PATTERN, payload: { value: order } }) this.clear() this.draw(false) } - clear () { + clear() { this.removeAllStepEventListeners() this.removeAllLayerEventListeners() if (!_.isNil(this.layerGrahpics)) { @@ -745,12 +552,12 @@ class PlayUI extends Component { if (!_.isNil(this.container)) { this.container.clear() } - if (!_.isNil(this.addLayerButton)) { - this.addLayerButton.click(null) + if (!_.isNil(this.playbackToggle)) { + this.playbackToggle.click(null) } } - reclaculateIndicatorAnimation () { + reclaculateIndicatorAnimation() { /* if (!_.isNil(this.positionLineAnimation)) { this.positionLineAnimation.unschedule() } @@ -763,10 +570,10 @@ class PlayUI extends Component { }*/ } - addLayer (layer, order, shouldAnimate = true) { - // console.log('addLayer', layer); - let animateTime = shouldAnimate ? 600 : 0 - + addLayer(layer, order, shouldAnimate = true) { + const dim = this.isRecordingSequence + // let animateTime = shouldAnimate ? 600 : 0 + const createdByThisUser = layer.createdBy === this.props.user.id; //const layerDiameter = HTML_UI_Params.addNewLayerButtonDiameter + HTML_UI_Params.initialLayerPadding + ((HTML_UI_Params.stepDiameter + HTML_UI_Params.layerPadding + HTML_UI_Params.layerPadding + HTML_UI_Params.stepDiameter) * (order + 1)) const layerDiameter = this.getLayerDiameter(order) const xOffset = (this.containerWidth / 2) - (layerDiameter / 2) @@ -775,7 +582,12 @@ class PlayUI extends Component { if (layer.createdBy === this.props.user.id) { layerStrokeSize = HTML_UI_Params.layerStrokeMax } - const layerGraphic = this.container.circle(layerDiameter, layerDiameter).attr({ fill: 'none' }).stroke({ color: this.userColors[layer.createdBy], width: layerStrokeSize + 'px', opacity: 0 }) + + const layerGraphic = + this.container.circle(layerDiameter, layerDiameter).attr({ fill: 'none' }) + .stroke({ color: this.userColors[layer.createdBy], width: layerStrokeSize + 'px' }) + .opacity(dim ? 0.1 : !createdByThisUser ? 0.5 : 1) + layer.isMuted && layerGraphic.stroke({ color: 'rgba(255,255,255,0.1)' }) layerGraphic.x(xOffset) layerGraphic.y(yOffset) layerGraphic.id = layer.id @@ -824,15 +636,16 @@ class PlayUI extends Component { const x = Math.round(layerDiameter / 2 + radius * Math.cos(angle) - stepDiameter / 2) + xOffset; const y = Math.round(layerDiameter / 2 + radius * Math.sin(angle) - stepDiameter / 2) + yOffset; const stepGraphic = this.container.circle(stepDiameter) - stepGraphic.stroke({ color: this.userColors[layer.createdBy], width: stepStrokeWidth + 'px', opacity: 0 }) - //stepGraphic.animate(animateTime).stroke({ opacity: 1 }) - stepGraphic.stroke({ opacity: 1 }) + stepGraphic.stroke({ + color: layer.isMuted ? 'rgba(255,255,255, 0.1)' : this.userColors[layer.createdBy], + width: stepStrokeWidth + 'px' + }).opacity(dim ? 0.1 : !createdByThisUser ? 0.5 : 1) stepGraphic.x(x) stepGraphic.y(y) angle += stepSize stepGraphic.layerId = layer.id stepGraphic.id = step.id - stepGraphic.isAllowedInteraction = layer.createdBy === this.props.user.id + stepGraphic.isAllowedInteraction = !dim && layer.createdBy === this.props.user.id stepGraphic.userColor = this.userColors[layer.createdBy] if (layer.createdBy === this.props.user.id) { stepGraphic.addClass(this.props.classes.button) @@ -846,33 +659,27 @@ class PlayUI extends Component { } layerGraphic.labelYOffset = 32 * (anglePercentOffset + angleTimeOffset) this.updateLayerLabel(layerGraphic) - } - getLayerDiameter (order) { - console.log('this.props.round.layers', this.props.round.layers); - let diameter = HTML_UI_Params.addNewLayerButtonDiameter + HTML_UI_Params.initialLayerPadding + getLayerDiameter(order) { + let diameter = HTML_UI_Params.addNewLayerButtonDiameter + (HTML_UI_Params.initialLayerPadding * 1.5) for (let i = 0; i < order; i++) { let layer = this.round.layers[i] if (layer.createdBy === this.props.user.id) { diameter += HTML_UI_Params.stepDiameter + HTML_UI_Params.layerPadding + HTML_UI_Params.layerPadding + HTML_UI_Params.stepDiameter - //diameter += HTML_UI_Params.stepDiameter + HTML_UI_Params.stepDiameter } else { diameter += ((HTML_UI_Params.stepDiameter + HTML_UI_Params.layerPadding + HTML_UI_Params.layerPadding + HTML_UI_Params.stepDiameter) / HTML_UI_Params.otherUserLayerSizeDivisor) - //diameter += ((HTML_UI_Params.stepDiameter + HTML_UI_Params.stepDiameter) / 2) } } - console.log('getLayerDiameter(' + order + ')', diameter); return diameter - //HTML_UI_Params.addNewLayerButtonDiameter + HTML_UI_Params.initialLayerPadding + ((HTML_UI_Params.stepDiameter + HTML_UI_Params.layerPadding + HTML_UI_Params.layerPadding + HTML_UI_Params.stepDiameter) * (order + 1)) } - updateLayerLabel (layerGraphic) { - layerGraphic.layerLabel.x(layerGraphic.firstStep.x() + HTML_UI_Params.stepDiameter + 8) - layerGraphic.layerLabel.y(layerGraphic.firstStep.y() + ((HTML_UI_Params.stepDiameter / 2) - 6) + layerGraphic.labelYOffset) + updateLayerLabel(layerGraphic) { + layerGraphic.layerLabel?.x(layerGraphic.firstStep?.x() + HTML_UI_Params.stepDiameter + 8) + layerGraphic.layerLabel?.y(layerGraphic.firstStep?.y() + ((HTML_UI_Params.stepDiameter / 2) - 6) + layerGraphic.labelYOffset) } - updateLayerLabelText (layerId, text) { + updateLayerLabelText(layerId, text) { if (text.length > 5) { text = text.substring(0, 5) + '...' } @@ -881,18 +688,20 @@ class PlayUI extends Component { this.updateLayerLabel(layerGraphic) } - updateStep (step, showActivityIndicator = false) { - // console.log('updateStep', step); + updateStep(step, showActivityIndicator = false) { if (!_.isEmpty(this.stepGraphics) && !_.isNil(step)) { const layer = this.stepLayerDictionary[step.id] const stepGraphic = _.find(this.stepGraphics, { id: step.id }) - //console.log('updating step', stepGraphic, step.isOn, this.props.user.color); const _this = this if (showActivityIndicator) { // add delay so that graphic updates after activity indicator hits it _.delay(() => { if (step.isOn) { - stepGraphic.animate(HTML_UI_Params.stepAnimationUpdateTime).attr({ fill: _this.userColors[layer.createdBy], 'fill-opacity': step.probability }) + stepGraphic.animate(HTML_UI_Params.stepAnimationUpdateTime).attr({ + fill: layer.isMuted ? 'rgba(255,255,255, 0.1)' : _this.userColors[layer.createdBy], + stroke: layer.isMuted ? 'rgba(255,255,255, 0.1)' : _this.userColors[layer.createdBy], + 'fill-opacity': step.probability + }) stepGraphic.animate(HTML_UI_Params.stepAnimationUpdateTime).transform({ scale: numberRange(step.velocity, 0, 1, 0.5, 1) }) @@ -902,9 +711,12 @@ class PlayUI extends Component { }, HTML_UI_Params.activityAnimationTime) this.animateActivityIndicator(layer.createdBy, stepGraphic.x() + (HTML_UI_Params.stepDiameter / 2), stepGraphic.y() + (HTML_UI_Params.stepDiameter / 2)) } else { - //console.log('updateStep()', step.isOn); if (step.isOn) { - stepGraphic.attr({ fill: _this.userColors[layer.createdBy], 'fill-opacity': step.probability }) + stepGraphic.attr({ + fill: layer.isMuted ? 'rgba(255,255,255, 0.1)' : _this.userColors[layer.createdBy], + stroke: layer.isMuted ? 'rgba(255,255,255, 0.1)' : _this.userColors[layer.createdBy], + 'fill-opacity': step.probability + }) stepGraphic.transform({ scale: numberRange(step.velocity, 0, 1, 0.5, 1) }) @@ -915,13 +727,12 @@ class PlayUI extends Component { } } - highlightLayer (layerGraphic, unhighlightExceptLayerId) { + highlightLayer(layerGraphic, unhighlightExceptLayerId) { this.unhighlightAllLayers(unhighlightExceptLayerId) - // layerGraphic.animate().stroke({ opacity: HTML_UI_Params.layerStrokeOpacity * 2 }) layerGraphic.stroke({ opacity: HTML_UI_Params.layerStrokeOpacity * 2 }) } - unhighlightAllLayers (exceptLayerId) { + unhighlightAllLayers(exceptLayerId) { for (const layerGraphic of this.layerGraphics) { if (layerGraphic.id !== exceptLayerId) { layerGraphic.stroke({ opacity: HTML_UI_Params.layerStrokeOpacity }) @@ -929,7 +740,7 @@ class PlayUI extends Component { } } - cacheStepLayers () { + cacheStepLayers() { this.stepLayerDictionary = {} for (let layer of this.props.round.layers) { for (let step of layer.steps) { @@ -938,18 +749,19 @@ class PlayUI extends Component { } } - adjustAllLayerOffsets () { + adjustAllLayerOffsets() { + let order = 0 for (const layer of this.round.layers) { - this.adjustLayerOffset(layer.id, layer.percentOffset, layer.timeOffset) + this.adjustLayerOffset(layer.id, layer.percentOffset, layer.timeOffset, order) + order++ } } - adjustLayerOffset (id, percentOffset, timeOffset) { - // console.log('adjustLayerTimeOffset', layer., percent, this.stepGraphics); + adjustLayerOffset(id, percentOffset, timeOffset, order) { const layer = _.find(this.round.layers, { id }) let stepGraphics = _.filter(this.stepGraphics, { layerId: id }) const layerGraphic = _.find(this.layerGraphics, { id }) - const layerDiameter = HTML_UI_Params.addNewLayerButtonDiameter + HTML_UI_Params.initialLayerPadding + ((HTML_UI_Params.stepDiameter + HTML_UI_Params.layerPadding + HTML_UI_Params.layerPadding + HTML_UI_Params.stepDiameter) * (layerGraphic.order + 1)) + const layerDiameter = this.getLayerDiameter(order) const xOffset = (this.containerWidth / 2) - (layerDiameter / 2) const yOffset = (this.containerHeight / 2) - (layerDiameter / 2) const stepSize = (2 * Math.PI) / layer.steps.length; @@ -975,19 +787,19 @@ class PlayUI extends Component { this.updateLayerLabel(layerGraphic) } - ticksPerStep (numberOfSteps) { + ticksPerStep(numberOfSteps) { const PPQ = Tone.Transport.PPQ const totalTicks = PPQ * 4 return Math.round(totalTicks / numberOfSteps) } - ticksToRadians (ticks) { + ticksToRadians(ticks) { const PPQ = Tone.Transport.PPQ const totalTicks = PPQ * 4 return ((Math.PI * 2) / totalTicks) * ticks } - msToTicks (ms) { + msToTicks(ms) { const BPM = Tone.Transport.bpm.value const PPQ = Tone.Transport.PPQ const msPerBeat = 60000 / BPM @@ -995,7 +807,7 @@ class PlayUI extends Component { return Math.round(ms / msPerTick) } - drawAvatars () { + drawAvatars() { if (!_.isNil(this.props.collaboration)) { this.avatarGraphics = [] const numberOfContributors = Object.entries(this.props.collaboration.contributors).length @@ -1014,7 +826,7 @@ class PlayUI extends Component { } } - updateAvatarPositions (numberOfLayers) { + updateAvatarPositions(numberOfLayers) { if (!_.isEmpty(this.avatarGraphics)) { let x = (this.containerWidth / 2) + (HTML_UI_Params.addNewLayerButtonDiameter / 2) + (HTML_UI_Params.initialLayerPadding / 2) + ((HTML_UI_Params.stepDiameter + HTML_UI_Params.layerPadding) * numberOfLayers) + HTML_UI_Params.avatarRoundPadding for (let avatarGraphic of this.avatarGraphics) { @@ -1023,7 +835,7 @@ class PlayUI extends Component { } } - animateActivityIndicator (userId, toX, toY) { + animateActivityIndicator(userId, toX, toY) { const avatarGraphic = _.find(this.avatarGraphics, { id: userId }) if (!_.isNil(this.activityIndicator) && !_.isNil(avatarGraphic)) { this.activityIndicator.fill({ color: this.userColors[userId], opacity: 1 }) @@ -1039,7 +851,7 @@ class PlayUI extends Component { } } - addLayerEventListeners (layerGraphic) { + addLayerEventListeners(layerGraphic) { const _this = this if (layerGraphic.isAllowedInteraction) { layerGraphic.click(function (e) { @@ -1051,12 +863,10 @@ class PlayUI extends Component { _this.onLayerClicked(layerGraphic.id) }) layerGraphic.on('mouseover', function (e) { - //console.log('layer mouseover'); e.stopPropagation() _this.onLayerOver(layerGraphic) }) layerGraphic.on('mouseout', function (e) { - //console.log('layer mouseout'); e.stopPropagation() _this.onLayerOut(layerGraphic) }) @@ -1066,36 +876,52 @@ class PlayUI extends Component { layerGraphic.on('touchend', (e) => { _this.onLayerTouchEnd(layerGraphic, e) }) + layerGraphic.on('dblclick', e => { + // should be a layer to mute toggle + this.onMuteToggle(this.props) + }) } } - onLayerTouchStart (layerGraphic, e) { + onLayerTouchStart(layerGraphic, e) { e.preventDefault() const _this = this this.layerTouchTimer = setTimeout(() => { _this.onLayerClicked(layerGraphic.id) }, 500) } - onLayerTouchEnd (layerGraphic) { + onLayerTouchEnd(layerGraphic) { if (this.layerTouchTimer) { clearTimeout(this.layerTouchTimer) } } - onLayerClicked (layerId) { + onLayerClicked(layerId) { this.selectedLayerId = layerId this.props.dispatch({ type: SET_SELECTED_LAYER_ID, payload: { layerId } }) this.props.dispatch({ type: SET_IS_SHOWING_LAYER_SETTINGS, payload: { value: true } }) this.highlightLayer(_.find(this.layerGraphics, { id: layerId })) } - onLayerOver (layerGraphic) { + onLayerOver(layerGraphic) { if (!this.swipeToggleActive) { this.highlightLayer(layerGraphic, this.selectedLayerId) } } - onLayerOut (layerGraphic) { + onLayerOut(layerGraphic) { this.unhighlightAllLayers(this.selectedLayerId) } - orderLayers () { + orderAndReturnLayers = async (layers) => { + let newLayers = _.sortBy(layers, 'createdAt') + let myLayers = _.filter(newLayers, { createdBy: this.props.user.id }) + myLayers = _.sortBy(myLayers, 'createdAt') + myLayers.reverse() + let collaboratorLayers = _.filter(newLayers, (layer) => { + return layer.createdBy !== this.props.user.id + }) + collaboratorLayers = _.sortBy(collaboratorLayers, ['createdBy', 'createdAt']) + return [...myLayers, ...collaboratorLayers] + } + + orderLayers() { // order layers this.round.layers = _.sortBy(this.round.layers, 'createdAt') let myLayers = _.filter(this.round.layers, { createdBy: this.props.user.id }) @@ -1108,47 +934,41 @@ class PlayUI extends Component { this.round.layers = [...myLayers, ...collaboratorLayers] } - orderSteps () { + orderSteps() { for (const layer of this.round.layers) { layer.steps = _.orderBy(layer.steps, 'order') } } - addStepEventListeners (stepGraphic) { - // console.log('addStepEventListeners'); + addStepEventListeners(stepGraphic) { this.removeStepEventListeners(stepGraphic) const _this = this if (stepGraphic.isAllowedInteraction) { + + stepGraphic.on('mouseout', async (e) => { + if (!_.isNil(_this.stepMoveTimer)) { + // we've swiped / dragged out of the step, toggle this step and listen for mouseovers on all other steps + // add listener to layergraphic to cancel swiping + _this.addStepSwipeListeners(stepGraphic) + _this.swipeToggleActive = true + _this.touchStartStepGraphic = stepGraphic + _this.onStepClick(stepGraphic) + } + }) + stepGraphic.on('mousedown', (e) => { - // console.log('mousedown'); e.stopPropagation() e.preventDefault() _this.swipeToggleActive = false _this.startStepMoveTimer(stepGraphic, e.pageX, e.pageY) - stepGraphic.on('mouseout', (e) => { - //console.log('mouseout'); - if (!_.isNil(_this.stepMoveTimer)) { - // we've swiped / dragged out of the step, toggle this step and listen for mouseovers on all other steps - // add listener to layergraphic to cancel swiping - _this.swipeToggleActive = true - _this.touchStartStepGraphic = stepGraphic - _this.onStepClick(stepGraphic) - _this.addStepSwipeListeners(stepGraphic) - - // _this.addStepSwipeCancelListener(stepGraphic) - } - - }) _this.container.on('mouseup', (e) => { - //console.log('_this.container.on(mouseup)'); e.stopPropagation() _this.removeStepSwipeListeners() _this.container.off('mousemove') _this.container.off('mouseup') _this.hideStepModal() - // console.log('mouseup', '_this.stepMoveTimer', _this.stepMoveTimer, '_this.swipeToggleActive', _this.swipeToggleActive); if (!_.isNil(_this.stepMoveTimer)) { // timer has not expired, so interpret as a click _this.clearShowStepModalTimer() @@ -1161,8 +981,8 @@ class PlayUI extends Component { this.swipeToggleActive = false }) }) + stepGraphic.on('touchstart', (e) => { - // console.log('touchstart'); e.stopPropagation() e.preventDefault() _this.swipeToggleActive = false @@ -1170,48 +990,43 @@ class PlayUI extends Component { _this.touchStartStepGraphic = stepGraphic _this.isCurrentlyOverStepGraphic = stepGraphic stepGraphic.on('touchmove', (e) => { - // console.log('touchmove'); + e.stopPropagation() + e.preventDefault() + this.isScrolling = true; if (_.isNil(_this.stepMoveTimer) && !_this.swipeToggleActive) { _this.onStepDragMove(stepGraphic, e.touches[0].pageX, e.touches[0].pageY) } else { - // console.log('touchmove', e, stepGraphic.id); - //_this.swipeToggleActive = stepGraphic _this.touchStartStepGraphic = stepGraphic _this.isOverStep(stepGraphic, e.touches[0].pageX, e.touches[0].pageY) } }) stepGraphic.on('touchend', (e) => { - // console.log('touchend'); + e.stopPropagation() + e.preventDefault() _this.hideStepModal() if (!_.isNil(_this.stepMoveTimer)) { // timer has not expired, so interpret as a click - // console.log('touchend interpreted as click', _this.swipeToggleActive); _this.clearShowStepModalTimer() if (!_this.swipeToggleActive) { - // console.log('acting on interpreted click'); _this.onStepClick(stepGraphic) - } else { - // console.log('ignoring interpreted click'); } - } else { _this.onStepDragEnd(stepGraphic) } stepGraphic.off('touchmove') stepGraphic.off('touchend') + _this.touchStartStepGraphic = null + _this.isScrolling = false + clearInterval() }) }) - // console.log('adding touchmove event for stepgraphic', stepGraphic.id); - - } } - removeStepEventListeners (stepGraphic) { - //console.log('removeStepEventListeners()'); + removeStepEventListeners(stepGraphic) { stepGraphic.off('mousedown') stepGraphic.off('touchstart') } - startStepMoveTimer (stepGraphic, x, y) { + startStepMoveTimer(stepGraphic, x, y) { const _this = this this.clearShowStepModalTimer() this.stepMoveTimer = setTimeout(function () { @@ -1222,7 +1037,7 @@ class PlayUI extends Component { }, 500) } - showStepModal (stepGraphic, pageX, pageY) { + showStepModal(stepGraphic, pageX, pageY) { this.clearShowStepModalTimer() this.stepModal.show() stepGraphic.startX = pageX @@ -1234,62 +1049,54 @@ class PlayUI extends Component { this.updateStepModal(stepGraphic) const _this = this this.container.on('mousemove', (e) => { - // console.log('_this.container.on(mousemove)'); e.preventDefault() _this.onStepDragMove(stepGraphic, e.pageX, e.pageY) }) } - hideStepModal () { - // console.log('hideStepModal()'); + hideStepModal() { this.stepModal.hide() this.container.off('mousemove') } - clearShowStepModalTimer () { - // console.log('clearShowStepModalTimer', this.stepMoveTimer); + clearShowStepModalTimer() { clearTimeout(this.stepMoveTimer) this.stepMoveTimer = null } - addStepSwipeListeners (originalStepGraphic) { - // console.log('addStepSwipeListeners', this); + addStepSwipeListeners(originalStepGraphic) { this.removeStepSwipeListeners() const _this = this for (const stepGraphic of this.stepGraphics) { if (stepGraphic.layerId === originalStepGraphic.layerId) { - // console.log('adding mouseover'); stepGraphic.on('mouseover', (e) => { - // console.log('on stepGraphic mouseover'); _this.onStepClick(stepGraphic) }) } } } - removeStepSwipeListeners () { + removeStepSwipeListeners() { for (const stepGraphic of this.stepGraphics) { stepGraphic.off('mouseout') stepGraphic.off('mouseover') } } - addStepSwipeCancelListener (stepGraphic) { + addStepSwipeCancelListener(stepGraphic) { const layerGraphic = _.find(this.layerGraphics, { id: stepGraphic.layerId }) const _this = this layerGraphic.on('mouseout', (e) => { - //console.log('layerGraphic mouseout'); _this.swipeToggleActive = false _this.removeStepSwipeListeners() layerGraphic.off('mouseout') }) } - onStepDragMove (stepGraphic, x, y) { + onStepDragMove(stepGraphic, x, y) { let deltaX = x - stepGraphic.startX let deltaY = y - stepGraphic.startY - //console.log('onStepDragMove', this.isZooming, stepGraphic.isOn, deltaX, deltaY, stepGraphic.isPanningX); if (!this.isZooming && stepGraphic.isOn) { if (deltaX < -100) { deltaX = -100 @@ -1306,8 +1113,6 @@ class PlayUI extends Component { deltaY = -100 } deltaY = deltaY / -100 - //delta += 1 - //stepGraphic.velocity = delta * stepGraphic.velocityPanStart; stepGraphic.velocity = stepGraphic.velocityPanStart + deltaY; if (stepGraphic.velocity < 0) { stepGraphic.velocity = 0 @@ -1326,46 +1131,44 @@ class PlayUI extends Component { } } - stepModalStepUpdate (stepGraphic) { + stepModalStepUpdate(stepGraphic) { let step = this.getStep(stepGraphic.id) step.probability = _.round(stepGraphic.probability, 1) step.velocity = _.round(stepGraphic.velocity, 1) - this.props.dispatch({ type: UPDATE_STEP, payload: { step: step, layerId: stepGraphic.layerId } }) this.saveLayer(stepGraphic.layerId) AudioEngine.recalculateParts(this.props.round) } - onStepDragEnd (stepGraphic) { + onStepDragEnd(stepGraphic) { if (stepGraphic.isOn) { const step = this.getStep(stepGraphic.id) step.probability = _.round(stepGraphic.probability, 1) - // this.props.dispatch({ type: SET_STEP_PROBABILITY, payload: { probability: step.probability, layerId: stepGraphic.layerId, stepId: stepGraphic.id, user: this.props.user.id } }) step.velocity = _.round(stepGraphic.velocity, 1) - // this.props.dispatch({ type: SET_STEP_VELOCITY, payload: { velocity: step.velocity, layerId: stepGraphic.layerId, stepId: stepGraphic.id, user: this.props.user.id } }) this.props.dispatch({ type: UPDATE_STEP, payload: { step: step, layerId: stepGraphic.layerId } }) this.saveLayer(stepGraphic.layerId) } AudioEngine.recalculateParts(this.props.round) } - highlightStep (stepGraphic) { + highlightStep(stepGraphic) { const layer = _.find(this.props.round.layers, { id: stepGraphic.layerId }) if (!_.isNil(layer)) { stepGraphic.animate(HTML_UI_Params.stepAnimationUpdateTime).attr({ fill: this.userColors[layer.createdBy], 'fill-opacity': 1 }) } } - unhighlightStep (stepGraphic) { + unhighlightStep(stepGraphic) { const step = this.getStep(stepGraphic.id) if (!step.isOn) { stepGraphic.animate(HTML_UI_Params.stepAnimationUpdateTime).attr({ fill: '#101114', 'fill-opacity': 1 }) } } - saveLayer (id) { - this.context.updateLayer(this.round.id, id, _.find(this.round.layers, { id })) + async saveLayer(id, round) { + const currentRound = round || this.props.round + await this.context.updateLayer(this.round.id, id, _.find(currentRound.layers, { id })) } - removeAllStepEventListeners () { + removeAllStepEventListeners() { for (let stepGraphic of this.stepGraphics) { stepGraphic.click(null) if (!_.isNil(stepGraphic.hammertime)) { @@ -1376,74 +1179,57 @@ class PlayUI extends Component { } } - removeAllLayerEventListeners () { + removeAllLayerEventListeners() { for (let layerGraphic of this.layerGraphics) { layerGraphic.click(null) } } - updateStepModal (stepGraphic) { - // console.log('updateStepModal', stepGraphic.probability, stepGraphic.velocity); - //this.stepModalText.text('Velocity: ' + _.round(stepGraphic.velocity, 1) + '\nProbability: ' + _.round(stepGraphic.probability, 1)) + updateStepModal(stepGraphic) { this.stepModal.x(stepGraphic.x() - ((HTML_UI_Params.stepModalDimensions / 2) - HTML_UI_Params.stepDiameter / 2)) this.stepModal.y(stepGraphic.y() - ((HTML_UI_Params.stepModalDimensions / 2) - HTML_UI_Params.stepDiameter / 2)) this.stepModalThumb.x(stepGraphic.probability * (HTML_UI_Params.stepModalDimensions - HTML_UI_Params.stepModalThumbDiameter)) this.stepModalThumb.y((1 - stepGraphic.velocity) * (HTML_UI_Params.stepModalDimensions - HTML_UI_Params.stepModalThumbDiameter)) } - onStepClick (stepGraphic) { + async onStepClick(stepGraphic) { + //const { user } = this.props let step = this.getStep(stepGraphic.id) - // console.log('onStepClick', step); - // update internal round so that it doesn't trigger another update when we receive a change after the dispatch step.isOn = !step.isOn this.updateStep(step, false) + this.props.dispatch({ type: TOGGLE_STEP, payload: { layerId: stepGraphic.layerId, stepId: stepGraphic.id, lastUpdated: new Date().getTime(), isOn: step.isOn, user: null } }) AudioEngine.recalculateParts(this.round) - this.props.dispatch({ type: TOGGLE_STEP, payload: { layerId: stepGraphic.layerId, stepId: stepGraphic.id, isOn: step.isOn, user: null } }) - // console.log('this.context', this.context); - this.saveLayer(stepGraphic.layerId) - //this.context.updateStep(this.round.id, stepGraphic.layerId, stepGraphic.id, step) - + await this.saveLayer(stepGraphic.layerId) + if (this.activePatternId) { + await this.onSavePattern(this.activePatternId) + } + this.draw() } - onAddLayerClick () { - const newLayer = getDefaultLayerData(this.props.user.id); + async onAddLayerClick() { + const newLayer = await getDefaultLayerData(this.props.user.id); newLayer.name = 'Layer ' + (this.props.round.layers.length + 1) this.props.dispatch({ type: ADD_LAYER, payload: { layer: newLayer, user: this.props.user.id } }) - this.context.createLayer(this.round.id, newLayer) this.highlightNewLayer = newLayer.id this.selectedLayerId = newLayer.id - /* const newLayer = _.cloneDeep(this.props.round.layers[this.props.round.layers.length - 1]) - newLayer.id = Math.round(Math.random() * 99999) - newLayer.order++; - for (const step of newLayer.steps) { - step.id = Math.round(Math.random() * 99999) - this.stepLayerDictionary[step.id] = newLayer - } - this.addLayer(newLayer, false) - AudioEngine.createTrack(newLayer)*/ - // this.draw() + this.context.createLayer(this.round.id, newLayer) } - addEventListeners () { - //const element = document.getElementById('round') - //const hammertime = new Hammer(element, {}); - //hammertime.get('pinch').set({ enable: true }); - } - addBackgroundEventListeners () { + addBackgroundEventListeners() { const element = document.getElementById('round') - element.addEventListener('click', this.onOutsideClick) + element && element.addEventListener('click', this.onOutsideClick) } - removeBackgroundEventListeners () { + removeBackgroundEventListeners() { const element = document.getElementById('round') - element.removeEventListener('click', this.onOutsideClick) + element && element.removeEventListener('click', this.onOutsideClick) } - onOutsideClick () { + onOutsideClick() { this.unhighlightAllLayers() this.props.dispatch({ type: SET_IS_SHOWING_LAYER_SETTINGS, payload: { value: false } }) this.selectedLayerId = null } - getUserColors () { + getUserColors() { let userColors = {}; for (const user of this.props.users) { userColors[user.id] = user.color @@ -1451,7 +1237,7 @@ class PlayUI extends Component { return userColors } - onWindowResize (e) { + onWindowResize(e) { const _this = this // some devices report incorrect orientation strightaway, however after around 500ms it seems to be correct. setTimeout(() => { @@ -1464,16 +1250,11 @@ class PlayUI extends Component { if (!_.isNil(_this.container)) { let width = window.innerWidth let height = window.innerHeight - // _this.containerWidth = Math.max(window.screen.width || 0, window.innerWidth || 0) - //_this.containerheight = Math.max(window.screen.height || 0, window.innerHeight || 0) _this.containerWidth = width _this.containerheight = height - - // console.log('onWindowResize', orientation, '_this.containerWidth', _this.containerWidth, '_this.containerheight', _this.containerheight); const roundElement = document.getElementById('round') roundElement.style.width = width + 'px' roundElement.style.height = height + 'px' - //console.log('set round height to ', height + 'px', 'actual height', roundElement.style.height); let currentViewBox = _this.container.viewbox() _this.container.size(width, height) @@ -1488,18 +1269,17 @@ class PlayUI extends Component { }, 500); } - getOrientation () { + getOrientation() { let orientation; if (window.orientation === 0 || window.orientation === 180) { orientation = 'portrait' } else { orientation = 'landscape' } - // console.log('getOrientation', orientation); return orientation } - checkOrientation () { + checkOrientation() { const _this = this _.delay(() => { if (_this.getOrientation() === 'portrait') { @@ -1510,42 +1290,44 @@ class PlayUI extends Component { }, 500) } - onKeypress (e) { - if (e.key === KEY_MAPPINGS.playToggle && !this.props.disableKeyListener) { - if (this.props.round.isPlaying) { - AudioEngine.stop() - this.context.updateRound(this.round.id, { isPlaying: false }) - this.props.dispatch({ type: SET_IS_PLAYING, payload: { value: false } }) - } else { - AudioEngine.play() - this.context.updateRound(this.round.id, { isPlaying: true }) - this.props.dispatch({ type: SET_IS_PLAYING, payload: { value: true } }) - } - } + onKeypress(e) { } - showOrientationDialog () { + showOrientationDialog() { this.props.dispatch({ type: SET_IS_SHOWING_ORIENTATION_DIALOG, payload: { value: true } }) } - hideOrientationDialog () { + hideOrientationDialog() { this.props.dispatch({ type: SET_IS_SHOWING_ORIENTATION_DIALOG, payload: { value: false } }) } + onPlaybackToggle = () => { + const { isPlaying, setIsPlaying } = this.props; + if (isPlaying) { + AudioEngine.stop() + setIsPlaying(false) + } else { + AudioEngine.play() + setIsPlaying(true) + } + this.draw() + } - isOverStep (initialStepGraphic, x, y) { - // console.log('checking is over step', x, y); + isOverStep(initialStepGraphic, x, y) { const _this = this let isOver = false for (const stepGraphic of this.stepGraphics) { if (stepGraphic.layerId === _this.touchStartStepGraphic.layerId) { - //console.log(stepGraphic, stepGraphic.x(), stepGraphic.y(), stepGraphic.node.getBoundingClientRect()); + const step = this.getStep(stepGraphic.id); const rect = stepGraphic.node.getBoundingClientRect() if (x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height) { - //console.log('is over step graphic'); isOver = true - if (_this.isCurrentlyOverStepGraphic !== stepGraphic) { - _this.onStepClick(stepGraphic) + const now = new Date().getTime() + const difference = step.lastUpdated ? (now - step.lastUpdated) : 0 + const secondsDifference = difference / 1000 + if (!_.isEqual(_this.isCurrentlyOverStepGraphic, stepGraphic) && + (secondsDifference === 0 || secondsDifference > 0.5)) { _this.isCurrentlyOverStepGraphic = stepGraphic + _this.onStepClick(stepGraphic) } } } @@ -1553,21 +1335,691 @@ class PlayUI extends Component { if (!isOver) { if (!_.isNil(this.isCurrentlyOverStepGraphic) && this.isCurrentlyOverStepGraphic === initialStepGraphic && !_.isNil(_this.stepMoveTimer)) { // just swiped off initial step - // console.log('clicking initial step'); _this.onStepClick(initialStepGraphic) } // we've swiped off the step so cancel the modal timer - // console.log('not over canceling timer'); this.clearShowStepModalTimer() this.isCurrentlyOverStepGraphic = null _this.swipeToggleActive = true } } - render () { - //console.log('HTML UI render()'); + renderPatternPresetsSequencer = async () => { + const { user, round } = this.props + this.clearPresetPatternsSequencer() + const userHasLayer = round.layers.find(layer => layer.createdBy === user.id) + const layerDiameter = !userHasLayer ? HTML_UI_Params.initialLayerDiameter : this.getLayerDiameter(1) + const patternsContainerDiameter = layerDiameter - HTML_UI_Params.patternsContainerDiameterOffset + + const xOffset = (this.containerWidth / HTML_UI_Params.patternsMainContainerDivisor) - (layerDiameter / HTML_UI_Params.patternsLayerDiameterDivisor) + const yOffset = (this.containerHeight / HTML_UI_Params.patternsMainContainerDivisor) - (layerDiameter / HTML_UI_Params.patternsLayerDiameterDivisor) + if (!_.isNil(round) && !_.isNil(round.userPatterns) && !_.isNil(round.userPatterns[user.id])) { + + this.renderPresetPatterns({ patternsContainerDiameter, xOffset, yOffset }) + this.renderSequences(); + const tempoButton = this.container.nested().rect(HTML_UI_Params.tempoButtonWidth, HTML_UI_Params.tempoButtonHeight).radius(HTML_UI_Params.tempoButtonRadius) + const tempoIcon = this.container.nested() + + tempoIcon.svg(` + + `) + + const tempoButtonX = xOffset + HTML_UI_Params.tempoButtonXOffset + const tempoButtonY = yOffset + HTML_UI_Params.tempoButtonYOffset + + const tempoIconX = xOffset + HTML_UI_Params.tempoIconXOffset + const tempoIconY = yOffset + HTML_UI_Params.tempoIconYOffset + + const tempoButtonTextX = xOffset + HTML_UI_Params.tempoButtonTextXOffset + const tempoButtonTextY = yOffset + HTML_UI_Params.tempoButtonTextYOffset + + tempoIcon.x(tempoIconX) + tempoIcon.y(tempoIconY) + tempoIcon.attr({ id: 'tempIcon' }) + + tempoButton.x(tempoButtonX) + tempoButton.y(tempoButtonY) + + tempoButton.fill('#fff').attr({ opacity: 0.1, id: 'tempo-button' }) + this.sequencerButtons.push(tempoButton) + this.sequencerButtons.push(tempoIcon) + const tempoButtonText = this.container.nested().plain(round.bpm) + + tempoButtonText.x(tempoButtonTextX) + tempoButtonText.y(tempoButtonTextY) + + tempoButtonText.font({ + family: 'Arial', + size: 11, + weight: 900, + opacity: 1, + }) + tempoButtonText.fill('#fff') + tempoButtonText.attr({ id: 'tempo-button-text' }) + + this.renderPlayingSequenceIndicator({ x: xOffset, y: yOffset }) + this.renderRecordSequenceButton(xOffset, yOffset) + } + } + + onLoadPattern = async (id) => { + if (!this.props.display.isRecordingSequence) { + const pattern = _.find(this.props.round.userPatterns[this.props.user.id].patterns, { id }) + if (!_.isEmpty(pattern.state)) { + this.setState({ selectedPattern: pattern.id }) + this.selectedPatternNeedsSaving = false + + // check if we have layers in the round not referenced in the pattern then set all steps in that layer to off + for (const existingLayer of this.props.round.layers) { + if (_.isNil(_.find(pattern.state.layers, { id: existingLayer.id })) && existingLayer.createdBy === this.props.user.id) { + let existingLayerClone = _.cloneDeep(existingLayer) + for (const step of existingLayerClone.steps) { + step.isOn = false + } + pattern.state.layers.push(existingLayerClone) + } + } + + // check we haven't deleted the layer that is referenced in the pattern + let layersToDelete = [] + for (const layer of pattern.state.layers) { + const layerExists = _.find(this.props.round.layers, { id: layer.id }) + if (_.isNil(layerExists)) { + layersToDelete.push(layer) + } + } + + _.remove(pattern.state.layers, function (n) { + return layersToDelete.indexOf(n) > -1 + }) + + await this.patternLayersToRound(pattern) + } + } else { + let seq = _.cloneDeep(this.props.round.userPatterns[this.props.user.id].sequence) + let firstAvailbleSlot = _.findIndex(seq, function (n) { + return n === false + }) + if (firstAvailbleSlot > -1) { + seq[firstAvailbleSlot] = id + this.props.setUserPatternSequence(this.props.user.id, seq) + !this.isRecordingSequence && !this.isPlayingSequence && this.context.saveUserPatterns(this.props.round.id, this.props.user.id, this.props.round.userPatterns[this.props.user.id]) + } else { + this.props.setIsRecordingSequence(false) + } + if (firstAvailbleSlot === seq.length - 1) { + this.onToggleRecordSequence() + this.props.setIsRecordingSequence(false) + this.props.setIsPlayingSequence(this.props.user.id, true) + } + /** set next available slot as current(highlighted) */ + if (!seq || firstAvailbleSlot < 0) return + this.props.setCurrentSequencePattern(firstAvailbleSlot) + } + } + + patternLayersToRound = async (pattern) => { + // make sure layers are ordered the same + let orderedLayers = [] + for (const layer of pattern.state.layers) { + let index = _.findIndex(this.props.round.layers, { id: layer.id }) + orderedLayers[index] = layer + } + await this.props.updateLayers(orderedLayers) + // now save to firebase + for (const layer of pattern.state.layers) { + // todo handle edge cases - eg layer been deleted + const layerExists = _.find(this.props.round.layers, { id: layer.id }) + if (!_.isNil(layerExists)) { + this.context.updateLayer(this.props.round.id, layer.id, layer) + } + } + } + + onRecordSequenceClick = () => { + if (!this.props.display.isRecordingSequence) { + // start write + this.props.setUserPatternSequence(this.props.user.id, getDefaultUserPatternSequence()) + this.isPlayingSequence = false + this.props.setIsPlayingSequence(this.props.user.id, false) + this.props.setCurrentSequencePattern(0) + } else { + // finish write + const { round, setIsPlayingSequence, setCurrentSequencePattern, user } = this.props + setCurrentSequencePattern(0) + const isPlayingSequence = true + this.isPlayingSequence = isPlayingSequence + setIsPlayingSequence(user.id, isPlayingSequence) + const newRound = { ...round } + newRound.userPatterns[user.id].isPlayingSequence = isPlayingSequence + this.context.saveUserPatterns(round.id, user.id, newRound.userPatterns[user.id]) + this.props.setIsPlayingSequence(this.props.user.id, true) + } + this.props.setIsRecordingSequence(!this.props.display.isRecordingSequence) + } + + onSavePattern = async (id) => { + this.setState({ selectedPattern: id }) + this.selectedPatternNeedsSaving = false + const state = this.getCurrentState(this.props.user.id) + this.props.saveUserPattern(this.props.user.id, id, state) + await this.context.saveUserPatterns(this.props.round.id, this.props.user.id, this.props.round.userPatterns[this.props.user.id]) + } + + getCurrentState = (userId) => { + /** Limit current state to current user layers **/ + const userLayers = _.filter(this.props.round.layers, { createdBy: userId }) + + //const layers = this.props.round.layers + let state = {} + state.layers = [] + for (const layer of userLayers) { + let stateLayer = { + id: layer.id, + createdBy: layer.createdBy, + createdAt: layer.createdAt, + steps: layer.steps, + gain: layer.gain, + isMuted: layer.isMuted, + timeOffset: layer.timeOffset, + percentOffset: layer.percentOffset + } + state.layers.push(stateLayer) + } + return state + } + + renderPlayingSequenceIndicator = ({ x, y }) => { + const { user } = this.props + const sequenceSwitch = this.container.nested().rect(HTML_UI_Params.sequenceSwitchWidth, HTML_UI_Params.sequenceSwitchHeight).radius(HTML_UI_Params.sequenceButtonRadius) + const switchLabelSubContainer = this.container.nested().circle(15) + const switchLabel = this.container.nested().plain('A') + const clickableSwitch = this.container.nested().rect(HTML_UI_Params.sequenceSwitchWidth, HTML_UI_Params.sequenceSwitchHeight).radius(HTML_UI_Params.sequenceButtonRadius) + const sequence = this.props.round.userPatterns[user.id].sequence + let dotAngle = Math.PI / HTML_UI_Params.anglePIDivisor + + switchLabel.font({ + family: 'Arial', + size: 11, + weight: 900, + opacity: 1 + }) + switchLabel.fill(user.color) + switchLabel.attr({ id: 'switch-letter' }) + this.microLayerGraphics.push(switchLabel) + + const sSwitchX = x + HTML_UI_Params.sequenceSwitchXOffset + const sSwitchY = y + HTML_UI_Params.sequenceSwitchYOffset + + for (let i = 0; i < HTML_UI_Params.sequenceButtonDots; i++) { + const dotSize = (2 * Math.PI) / sequence.length + let dotDiameter = HTML_UI_Params.dotDiameter - HTML_UI_Params.sequenceSwitchDotOffset + dotAngle += dotSize + const switchDotsDiameter = HTML_UI_Params.sequenceButtonDiameter - HTML_UI_Params.sequenceSwitchDotsDiameterOffset + const radius = switchDotsDiameter / 2; + + const bSX = (Math.round(radius + (radius * Math.cos(dotAngle)) - dotDiameter / 2) + sSwitchX) + HTML_UI_Params.sequenceSwitchDotsXOffset + const bSY = (Math.round(radius + (radius * Math.sin(dotAngle)) - dotDiameter / 2) + sSwitchY) + HTML_UI_Params.sequenceSwitchDotsYOffset + + const sequenceSwitchDot = this.container.nested().circle(dotDiameter) + sequenceSwitchDot.attr({ id: `${i}_sequence_dot`, fill: 'rgba(0,0,0,0.1)', opacity: 1 }) + sequenceSwitchDot.stroke({ color: user.color, width: 1 }) + sequenceSwitchDot.x(bSX) + sequenceSwitchDot.y(bSY) + this.microLayerGraphics.push(sequenceSwitchDot) + } + + sequenceSwitch.stroke({ width: 0.3, color: user.color }) + sequenceSwitch.fill({ + color: '#000', + opacity: 0.001 + }) + sequenceSwitch.attr({ + id: 'sequence-switch', + cursor: 'pointer' + }) + sequenceSwitch.x(sSwitchX) + sequenceSwitch.y(sSwitchY) + this.setIsPlayingSequenceGraphic({ x, y }) + switchLabelSubContainer.attr({ + id: 'switch-letter-subcontainer' + }) + switchLabelSubContainer.fill('none') + switchLabelSubContainer.stroke({ color: user.color, width: 0.5 }) + const switchLabelSubContainerX = x + HTML_UI_Params.sequenceSwitchLabelSubContainerXOffset + const switchLabelSubContainerY = y + HTML_UI_Params.sequenceSwitchLabelSubContainerYOffset + const switchLabelX = x + HTML_UI_Params.sequenceSwitchLabelXOffset + const switchLabelY = y + HTML_UI_Params.sequenceSwitchLabelYOffset + switchLabelSubContainer.x(switchLabelSubContainerX) + switchLabelSubContainer.y(switchLabelSubContainerY) + switchLabel.x(switchLabelX) + switchLabel.y(switchLabelY) + + /** clickable button */ + clickableSwitch.fill({ + color: '#000', + opacity: 0.001 + }) + clickableSwitch.attr({ + id: 'clickable-switch', + cursor: 'pointer' + }) + clickableSwitch.on('click', this.toggleIsPlayingSequence) + clickableSwitch.x(sSwitchX) + clickableSwitch.y(sSwitchY) + this.microLayerGraphics.push(sequenceSwitch) + this.microLayerGraphics.push(clickableSwitch) + this.microLayerGraphics.push(switchLabel) + this.microLayerGraphics.push(switchLabelSubContainer) + } + + toggleIsPlayingSequence = () => { + const { round, setIsPlayingSequence, setCurrentSequencePattern, user } = this.props + setCurrentSequencePattern(0) + const isPlayingSequence = !this.isPlayingSequence + setIsPlayingSequence(user.id, isPlayingSequence) + const newRound = { ...round } + newRound.userPatterns[user.id].isPlayingSequence = isPlayingSequence + this.context.saveUserPatterns(round.id, user.id, newRound.userPatterns[user.id]) + this.isPlayingSequence = isPlayingSequence + } + + setIsPlayingSequenceGraphic = ({ x, y }) => { + const { user, round } = this.props + const isPlayingSequence = round.userPatterns[this.props.user.id].isPlayingSequence + const switchLabelContainer = this.container.nested().circle(HTML_UI_Params.sequenceSwitchLabelContainerSize) + let switchLabelContainerX = x + HTML_UI_Params.sequenceSwitchLabelContainerOffXOffset + let switchLabelContainerY = y + HTML_UI_Params.sequenceSwitchLabelContainerYOffset + + if (isPlayingSequence) { + switchLabelContainerX = x + HTML_UI_Params.sequenceSwitchLabelContainerONXOffset + } + + switchLabelContainer.x(switchLabelContainerX) + switchLabelContainer.y(switchLabelContainerY) + switchLabelContainer.fill(user.color) + switchLabelContainer.attr({ + id: 'switch-letter-container', + opacity: 0.2 + }) + } + + renderPresetPatterns = async ({ patternsContainerDiameter, xOffset, yOffset }) => { + const { round, user } = this.props + const patterns = round.userPatterns[user.id].patterns + let angle = Math.PI / HTML_UI_Params.anglePIDivisor + let i = 0 + for (const pattern of patterns) { + const { state: { layers }, id } = pattern + const patternSize = (2 * Math.PI) / patterns.length + let patternDiameter = HTML_UI_Params.stepDiameter + const isSelected = id === this.activePatternId + const opacity = isSelected ? 1 : 0.2 + angle += patternSize + const letter = PRESET_LETTERS[pattern.order] + const radius = patternsContainerDiameter / 2; + + const x = (Math.round(patternsContainerDiameter / 2 + radius * Math.cos(angle) - patternDiameter / 2) + xOffset) + const y = (Math.round(patternsContainerDiameter / 2 + radius * Math.sin(angle) - patternDiameter / 2) + yOffset) + + const currentPatternGraphic = this.container.nested().circle(patternDiameter) + const label = this.container.nested().plain(letter).attr({ cursor: 'pointer' }) + label.font({ + family: 'Arial', + size: 25, + weight: 900, + opacity: isSelected ? 1 : 0.6 + }) + const labelX = x + HTML_UI_Params.presetLabelXOffset + const labelY = y + HTML_UI_Params.presetLabelYOffset + label.fill({ color: user.color }) + label.attr({ id: `${i}_pattern_label` }) + label.x(labelX) + label.y(labelY) + + currentPatternGraphic.attr({ id: `${i}_pattern`, fill: 'none', opacity: isSelected ? 0.3 : 0.15, cursor: 'pointer' }) + currentPatternGraphic.stroke({ color: user.color, width: 18 }) + currentPatternGraphic.fill('none') + currentPatternGraphic.x(x) + currentPatternGraphic.y(y) + this.microPatternGraphics.push(currentPatternGraphic) + this.microLayerGraphics.push(label) + + if (isSelected) { + const patternOutline = this.container.nested().circle(patternDiameter + HTML_UI_Params.presetPatternOulineDiameterOffset) + patternOutline.stroke({ + color: user.color, width: 2 + }).fill('none').opacity(1) + const patternOutlineX = x - HTML_UI_Params.presetPatternOutlineXOffset + const PatternOutlineY = y - HTML_UI_Params.presetPatternOutlineYOffset + patternOutline.x(patternOutlineX) + patternOutline.y(PatternOutlineY) + patternOutline.attr({ id: `${i}-pattern-outline` }) + this.microLayerGraphics.push(patternOutline) + } + if (layers && layers.length > 0) { + this.renderMicroRound({ x: x + 1.5, y: y + 1.5, pattern: currentPatternGraphic, isFilled: isSelected, layers, opacity }) + } + const clickableButtonDiameter = patternDiameter + HTML_UI_Params.presetClickableButtonDiameterOffset + const clickableButton = this.container.nested().circle(clickableButtonDiameter) + clickableButton.fill({ color: '#000', opacity: 0.001 }) + clickableButton.attr({ cursor: 'pointer', id: `${i}_pattern_clickable_button` }) + const clickableButtonX = x - HTML_UI_Params.presetClickableButtonXOffset + const clickableButtonY = y - HTML_UI_Params.presetClickableButtonYoffset + clickableButton.x(clickableButtonX) + clickableButton.y(clickableButtonY) + this.microLayerGraphics.push(clickableButton) + clickableButton.on('click', async () => { + const { round, isPlaying } = this.props + const patterns = round.userPatterns[user.id].patterns + if (this.isPlayingSequence && isPlaying) return + if (!this.isRecordingSequence) { + this.activePatternId = id + const pattern = _.find(patterns, { id }) + const patternLayers = pattern.state.layers + if (!patternLayers) { + pattern.state.layers = [] + /** clear out steps from existing layers */ + for (const existingLayer of round.layers) { + let existingLayerClone = _.cloneDeep(existingLayer) + for (const step of existingLayerClone.steps) { + step.isOn = false + } + pattern.state.layers.push(existingLayerClone) + } + this.props.dispatch({ type: UPDATE_LAYERS, payload: { layers: pattern.state.layers } }) + await this.onSavePattern(id) + } + + if (patternLayers) { + this.onLoadPattern(id) + } + AudioEngine.recalculateParts(this.props.round) + this.draw() + } + if (layers && layers.length > 0 && this.isRecordingSequence) { + this.activePatternId = id + this.onLoadPattern(id) + this.draw() + } + }) + i++ + } + } + + renderSequences = async () => { + const { round, user } = this.props + const sequence = round.userPatterns[this.props.user.id].sequence + const userHasLayer = round.layers.find(layer => layer.createdBy === user.id) + const layerDiameter = !userHasLayer ? HTML_UI_Params.initialLayerDiameter : this.getLayerDiameter(1) + const sequenceContainerDiameter = layerDiameter - HTML_UI_Params.sequenceContainerDiameterOffset + const xOffset = (this.containerWidth / 2) - (layerDiameter / HTML_UI_Params.patternsLayerDiameterDivisor) + const yOffset = (this.containerHeight / 2) - (layerDiameter / HTML_UI_Params.patternsLayerDiameterDivisor) + + let sAngle = Math.PI / HTML_UI_Params.anglePIDivisor + let i = 0 + for (const id of sequence) { + const isFilled = id + const patterns = round.userPatterns[this.props.user.id].patterns + const pattern = patterns.find(pattern => pattern.id === id); + const isHighlighted = i === this.props.display.currentSequencePattern + const opacity = isHighlighted ? 1 : 0.2 + + const sequenceSize = (2 * Math.PI) / sequence.length + let sequenceDiameter = HTML_UI_Params.stepDiameter - HTML_UI_Params.sequenceDiameterOffset + sAngle += sequenceSize + const radius = sequenceContainerDiameter / 2; + + const sX = (Math.round(radius + (radius * Math.cos(sAngle)) - sequenceDiameter / HTML_UI_Params.patternsMainContainerDivisor) + xOffset) + HTML_UI_Params.sequencePatternXOffset + const sY = (Math.round(radius + (radius * Math.sin(sAngle)) - sequenceDiameter / HTML_UI_Params.patternsMainContainerDivisor) + yOffset) + HTML_UI_Params.sequencePatternYOffset + + const sequencePattern = this.container.nested().circle(sequenceDiameter) + + if (pattern) { + const letter = PRESET_LETTERS[pattern.order] + const label = this.container.nested().plain(letter).attr({ cursor: 'pointer' }) + label.font({ + family: 'Arial', + size: 10, + weight: 900, + opacity: 1 + }) + const labelX = sX + HTML_UI_Params.sequenceLabelXOffset + const labelY = sY + HTML_UI_Params.sequenceLabelYOffset + label.fill({ color: user.color }) + label.x(labelX) + label.y(labelY) + } + + if (isFilled) { + const sequenceBackgroundDiameter = sequenceDiameter - HTML_UI_Params.sequenceBackgroundDiameterOffset + const sequenceBackground = this.container.nested().circle(sequenceBackgroundDiameter) + sequenceBackground.attr({ id: `${i}_sequence_bg` }) + sequenceBackground.stroke({ color: user.color, width: HTML_UI_Params.sequenceBackgroundWidth, opacity: isHighlighted && pattern ? 0.3 : 0.1 }) + sequenceBackground.fill({ + color: 'rgba(0,0,0,0.01)' + }) + const sequencBackgroundX = sX + HTML_UI_Params.sequenceBackgroundXOffset + const sequencBackgroundY = sY + HTML_UI_Params.sequenceBackgroundYOffset + sequenceBackground.x(sequencBackgroundX) + sequenceBackground.y(sequencBackgroundY) + } + sequencePattern.attr({ id: `${i}_sequence_pattern` }) + sequencePattern.stroke({ color: user.color, width: 1, opacity: isHighlighted || this.isRecordingSequence ? 1 : 0.2 }) + sequencePattern.fill('none') + sequencePattern.x(sX) + sequencePattern.y(sY) + const layers = pattern && pattern.state && [...pattern.state.layers] + + if (layers) { + this.renderMicroRound({ + x: sX + HTML_UI_Params.patternsLayerDiameterDivisor, + y: sY + HTML_UI_Params.patternsLayerDiameterDivisor, + pattern: sequencePattern, + layers, + opacity, + isFilled: isHighlighted, + diameter: sequenceDiameter + }) + } + this.sequenceGraphics.push(sequencePattern) + i++ + } + } + + renderRecordSequenceButton = (xOffset, yOffset) => { + const { round, user } = this.props + const sequence = round.userPatterns[user.id].sequence + let dotAngle = Math.PI / HTML_UI_Params.anglePIDivisor + + const sButtonX = xOffset + HTML_UI_Params.sequenceButtonXOffset + const sButtonY = yOffset + HTML_UI_Params.sequenceButtonYOffset + + if (!this.isRecordingSequence) { + const sequenceButton = this.container.nested().rect(HTML_UI_Params.sequenceButtonWidth, HTML_UI_Params.sequenceButtonHeight).radius(HTML_UI_Params.sequenceButtonRadius) + sequenceButton.attr({ id: 'sequence-button', fill: user.color, opacity: 0.2 }) + sequenceButton.x(sButtonX) + sequenceButton.y(sButtonY) + for (let i = 0; i < HTML_UI_Params.sequenceButtonDots; i++) { + const dotSize = (2 * Math.PI) / sequence.length + let dotDiameter = HTML_UI_Params.dotDiameter + dotAngle += dotSize + const radius = HTML_UI_Params.sequenceButtonDiameter / 2; + + const bSX = (Math.round(radius + (radius * Math.cos(dotAngle)) - dotDiameter / 2) + sButtonX) + HTML_UI_Params.dotXOffset + const bSY = (Math.round(radius + (radius * Math.sin(dotAngle)) - dotDiameter / 2) + sButtonY) + HTML_UI_Params.dotYOffset + + const sequenceButtonDots = this.container.nested().circle(dotDiameter) + sequenceButtonDots.attr({ id: `${i}-sbuttonDot`, fill: 'rgba(0,0,0,0.1)', opacity: 1 }) + sequenceButtonDots.stroke({ color: user.color, width: 1 }) + sequenceButtonDots.x(bSX) + sequenceButtonDots.y(bSY) + this.microLayerGraphics.push(sequenceButtonDots) + } + const sequenceText = this.container.nested().plain('Sequence').font({ + family: 'Arial', + size: 11, + weight: 900, + opacity: 1 + }) + sequenceText.attr({ id: 'sequence-text', cursor: 'pointer' }) + sequenceText.fill(user.color) + const sTextX = xOffset + HTML_UI_Params.sequenceTextXOffset + const sTextY = yOffset + HTML_UI_Params.sequenceTextYOffset + sequenceText.x(sTextX) + sequenceText.y(sTextY) + const clickableSequenceButton = this.container.nested().rect(HTML_UI_Params.sequenceButtonWidth, HTML_UI_Params.sequenceButtonHeight).radius(HTML_UI_Params.sequenceButtonRadius) + clickableSequenceButton.on('click', this.onToggleRecordSequence) + clickableSequenceButton.attr({ id: 'sequence-cickable-button', fill: '#000', opacity: 0.00001, cursor: 'pointer' }) + clickableSequenceButton.x(sButtonX) + clickableSequenceButton.y(sButtonY) + this.microLayerGraphics.push(sequenceButton) + this.microLayerGraphics.push(sequenceText) + this.microLayerGraphics.push(clickableSequenceButton) + } + + if (this.isRecordingSequence) { + const sStopIconX = xOffset + HTML_UI_Params.stopSequenceIconXOffset + const sStopIconY = yOffset + HTML_UI_Params.stopSequenceIconYOffset + const sStopButtonX = xOffset + HTML_UI_Params.stopSequenceButtonXOffset + const sStopButtonY = yOffset + HTML_UI_Params.stopSequenceButtonYOffset + const sequenceButton = this.container.nested().rect(HTML_UI_Params.stopSequenceButtonWidth, HTML_UI_Params.stopSequenceButtonHeight).radius(HTML_UI_Params.sequenceButtonRadius) + sequenceButton.attr({ id: 'sequence-button', fill: user.color, opacity: 0.2 }) + sequenceButton.x(sStopButtonX) + sequenceButton.y(sStopButtonY) + const sequenceStop = this.container.nested().svg(` + + `) + sequenceStop.attr({ id: 'sequence-stop', fill: user.color, opacity: 1 }) + sequenceStop.stroke({ color: user.color, width: 1 }) + sequenceStop.x(sStopIconX) + sequenceStop.y(sStopIconY) + const sequenceText = this.container.nested().plain('Stop').font({ + family: 'Arial', + size: 11, + weight: 900, + opacity: 1 + }) + sequenceText.attr({ id: 'sequence-text', cursor: 'pointer' }) + sequenceText.fill(user.color) + const sTextX = xOffset + HTML_UI_Params.stopSequenceTextXOffset + const sTextY = yOffset + HTML_UI_Params.stopSequenceTextYOffset + sequenceText.x(sTextX) + sequenceText.y(sTextY) + const clickableSequenceButton = this.container.nested().rect(HTML_UI_Params.stopSequenceButtonWidth, HTML_UI_Params.stopSequenceButtonHeight).radius(HTML_UI_Params.sequenceButtonRadius) + clickableSequenceButton.on('click', this.onToggleRecordSequence) + clickableSequenceButton.attr({ id: 'sequence-button', fill: '#000', opacity: 0.00001, cursor: 'pointer' }) + clickableSequenceButton.x(sStopButtonX) + clickableSequenceButton.y(sStopButtonY) + this.microLayerGraphics.push(sequenceText) + this.microLayerGraphics.push(sequenceButton) + this.microLayerGraphics.push(sequenceStop) + this.microLayerGraphics.push(clickableSequenceButton) + } + } + + onToggleRecordSequence = () => { + const { isPlaying } = this.props + if (isPlaying) + this.onPlaybackToggle() + this.isRecordingSequence = !this.isRecordingSequence + this.onRecordSequenceClick() + this.renderPatternPresetsSequencer() + } + + clearPresetPatternsSequencer = () => { + for (let graphic of this.microPatternGraphics) { + graphic.clear() + } + for (let graphic of this.sequenceGraphics) { + graphic.clear() + } + this.clearPresetGraphics() + } + + clearPresetGraphics = () => { + for (let graphic of this.microStepGraphics) { + graphic.clear() + } + for (let graphic of this.microLayerGraphics) { + graphic.clear() + } + for (let graphic of this.sequencerButtons) { + graphic.clear() + } + } + + getMicroLayerDiameter(order, dm) { + let diameter = dm ? 3 + (HTML_UI_Params.initialMicro2LayerPadding * 1.4) : 5 + (HTML_UI_Params.initialMicroLayerPadding * 1.4) + const stepDiameter = dm ? HTML_UI_Params.micro2StepDiameter : HTML_UI_Params.microStepDiameter + for (let i = 0; i < order; i++) { + diameter += stepDiameter + HTML_UI_Params.microLayerPadding + } + return diameter + } + + addMicroLayer = async (layer, order, { containerXOffset, containerYOffset, diameter, isFilled }) => { + const { user } = this.props + const layerDiameter = this.getMicroLayerDiameter(order, diameter) + const xOffset = containerXOffset + 6 - (order * (diameter ? HTML_UI_Params.micro2LayerOffsetMultiplier : HTML_UI_Params.microLayerOffsetMultiplier)) + const yOffset = containerYOffset + 6 - (order * (diameter ? HTML_UI_Params.micro2LayerOffsetMultiplier : HTML_UI_Params.microLayerOffsetMultiplier)) + const layerStrokeSize = diameter ? HTML_UI_Params.micro2LayerStrokeMax : HTML_UI_Params.microLayerStrokeMax + const layerGraphic = + this.container.circle(layerDiameter).fill('none') + .stroke({ color: user.color, width: layerStrokeSize, opacity: 0.00001 }) + layerGraphic.x(xOffset) + layerGraphic.y(yOffset) + layerGraphic.id = layer.id + layerGraphic.order = order + layerGraphic.isAllowedInteraction = false + this.microLayerGraphics.push(layerGraphic) + + // draw steps + const stepSize = (2 * Math.PI) / layer.steps.length; + let stepDiameter = HTML_UI_Params.microStepDiameter / HTML_UI_Params.otherUserLayerSizeDivisor + const radius = layerDiameter / 2 + let angle = Math.PI / -2 + const anglePercentOffset = this.ticksToRadians(this.ticksPerStep(layer.steps.length) * (layer.percentOffset / 100)) + const angleTimeOffset = this.ticksToRadians(this.msToTicks(layer.timeOffset)) + angle += anglePercentOffset + angle += angleTimeOffset + layerGraphic.firstStep = null + await layer.steps.map((step, i) => { + const { id } = step + const x = Math.round(layerDiameter / 2 + radius * Math.cos(angle) - stepDiameter / 2) + xOffset; + const y = Math.round(layerDiameter / 2 + radius * Math.sin(angle) - stepDiameter / 2) + yOffset; + const stepGraphic = this.container.circle(stepDiameter) + stepGraphic.stroke('none') + stepGraphic.fill({ color: step.isOn ? user.color : 'rgba(0,0,0,0)', opacity: isFilled ? 1 : 0.5 }) + stepGraphic.attr({ id: `micro-step-${id}` }) + stepGraphic.x(x) + stepGraphic.y(y) + angle += stepSize + stepGraphic.layerId = layer.id + stepGraphic.id = step.id + stepGraphic.isAllowedInteraction = false + stepGraphic.userColor = user.color + this.microStepGraphics.push(stepGraphic) + if (_.isNil(layerGraphic.firstStep)) { + layerGraphic.firstStep = stepGraphic + } + return null + }) + layerGraphic.labelYOffset = 32 * (anglePercentOffset + angleTimeOffset) + } + + renderMicroRound = async ({ x, y, pattern, layers, isFilled, diameter }) => { + if (this.activePattern === pattern) return + const sortedLayers = await this.orderAndReturnLayers(layers) + sortedLayers && sortedLayers.map(async (layer, i) => { + return await this.addMicroLayer(layer, i++, { containerXOffset: x, containerYOffset: y, diameter, isFilled }) + }) + this.activePattern = pattern + } + + render() { return ( -
    +
    ) } } @@ -1576,16 +2028,35 @@ PlayUI.propTypes = { }; const mapStateToProps = state => { - //console.log('mapStateToProps', state); + let selectedLayer = null; + if (!_.isNil(state.display.selectedLayerId) && !_.isNil(state.round) && !_.isNil(state.round.layers)) { + selectedLayer = _.find(state.round.layers, { id: state.display.selectedLayerId }) + } return { round: state.round, user: state.user, users: state.users, + display: state.display, + selectedLayer, + isPlaying: !_.isNil(state.round) && state.round.isPlaying ? true : false, + selectedLayerId: state.display.selectedLayerId, disableKeyListener: state.display.disableKeyListener }; -}; +} + +const mapDispatchToProps = dispatch => ({ + setIsPlaying: val => dispatch(setIsPlaying(val)), + setIsRecordingSequence: val => dispatch(setIsRecordingSequence(val)), + setUserPatternSequence: (userId, data) => dispatch(setUserPatternSequence(userId, data)), + updateLayers: (layers) => dispatch(updateLayers(layers)), + setIsPlayingSequence: (userId, val) => dispatch(setIsPlayingSequence(userId, val)), + saveUserPattern: (userId, patternId, data) => dispatch(saveUserPattern(userId, patternId, data)), + setCurrentSequencePattern: val => dispatch(setCurrentSequencePattern(val)), + dispatch +}) export default connect( - mapStateToProps + mapStateToProps, + mapDispatchToProps )(withStyles(styles)(PlayUI)); diff --git a/src/components/play/layer-settings/DeleteClearPopup.js b/src/components/play/layer-settings/DeleteClearPopup.js new file mode 100644 index 0000000..e642d1f --- /dev/null +++ b/src/components/play/layer-settings/DeleteClearPopup.js @@ -0,0 +1,33 @@ +import React from 'react' +import Box from '@material-ui/core/Box' +import IconButton from '@material-ui/core/IconButton' +import { + ErasorIcon, + TrashIcon +} from './resources' +import { Typography } from '@material-ui/core' + +export default function DeleteClearPopup({ + classes, + onClearStepsClick, + showDeleteClearPopup, + onDeleteLayerClick +}) { + return ( + + + Clear + + + + Delete + + + + ) +} diff --git a/src/components/play/layer-settings/HamburgerPopup.js b/src/components/play/layer-settings/HamburgerPopup.js new file mode 100644 index 0000000..26f53bc --- /dev/null +++ b/src/components/play/layer-settings/HamburgerPopup.js @@ -0,0 +1,44 @@ +import React from 'react' +import Box from '@material-ui/core/Box' +import IconButton from '@material-ui/core/IconButton' +import { + PlusIcon, + EqualiserIcon +} from './resources' +import { Typography } from '@material-ui/core' + +export default function HamburgerPopup({ + classes, + user, + userColors, + showMixerPopup, + showHamburgerPopup, + addLayerButtonRef, + mixerPopupButtonRef, + toggleShowMixerPopup, + onAddLayerClick +}) { + return ( + + + Add round + + + + Mixer + + + + ) +} diff --git a/src/components/play/layer-settings/LayerCustomSounds.js b/src/components/play/layer-settings/LayerCustomSounds.js index 7ab8a72..936ab42 100644 --- a/src/components/play/layer-settings/LayerCustomSounds.js +++ b/src/components/play/layer-settings/LayerCustomSounds.js @@ -3,8 +3,6 @@ import { withStyles } from '@material-ui/styles'; import Box from '@material-ui/core/Box'; import Button from '@material-ui/core/Button'; import { connect } from "react-redux"; -import MicIcon from '@material-ui/icons/MicOutlined'; -import StopIcon from '@material-ui/icons/Stop'; import UploadIcon from '@material-ui/icons/CloudUploadOutlined'; import AudioEngine from '../../../audio-engine/AudioEngine' import AudioRecorder from '../../../audio-engine/AudioRecorder' @@ -19,11 +17,12 @@ import Dropzone from 'react-dropzone' const styles = theme => ({ root: { + display: 'flex', width: '100%', padding: theme.spacing(1) }, buttonContainer: { - width: '100%', + flex: 1, marginBottom: theme.spacing(2), display: 'flex', justifyContent: 'space-between', @@ -31,7 +30,10 @@ const styles = theme => ({ flexDirection: "column" } }, - + fullWidthFlex: { + display: 'flex', + flex: 1 + }, button: { minWidth: 130, height: 36, @@ -41,19 +43,17 @@ const styles = theme => ({ } }, uploadButton: { - minWidth: 130, - height: 36, + flex: 1, + width: '100%', + minWidth: '100%', border: '1px dashed rgba(255,255,255,0.2)', - [theme.breakpoints.down('sm')]: { - minWidth: 118 - } }, }) class LayerCustomSounds extends Component { static contextType = FirebaseContext - constructor (props) { + constructor(props) { super(props) this.state = { mode: null, @@ -78,7 +78,7 @@ class LayerCustomSounds extends Component { /*componentDidMount () { window.addEventListener('drop', this.onDropFile); }*/ - async onRecordClick () { + async onRecordClick() { // console.log('onRecordClick', this.state.mode) if (_.isNil(this.state.mode)) { if (!AudioEngine.isOn()) { @@ -102,18 +102,18 @@ class LayerCustomSounds extends Component { // console.log('ignoring click'); } } - onCountDown (value) { + onCountDown(value) { this.setState({ recordButtonText: value }) } - onRecordLevel (level) { + onRecordLevel(level) { this.setState({ level }) } - onRecordingStarted () { + onRecordingStarted() { this.setState({ mode: 'recording', recordButtonText: 'Recording' }) } - async onRecordingFinished (blob) { + async onRecordingFinished(blob) { // console.log('recording finsished'); this.setState({ mode: 'upload' }) @@ -150,11 +150,11 @@ class LayerCustomSounds extends Component { } - onCustomSampleFileUploaderChange () { + onCustomSampleFileUploaderChange() { } - async onDropFile (files) { + async onDropFile(files) { // console.log('onDropFile', files); const file = files?.[0] if (!file) { @@ -197,46 +197,20 @@ class LayerCustomSounds extends Component { } } - render () { + render() { //console.log('########### render()', this.state.mode); const { classes } = this.props; - let startIcon = this.state.mode === 'recording' ? : let uploadStartIcon = this.state.mode === 'fileUpload' ? '' : - let recordButtonColor = (this.state.mode === 'recording') ? 'red' : 'white' - if (this.state.mode === 'countdown' || this.state.mode === 'upload' || this.state.mode === 'fileUpload') { - startIcon = '' - recordButtonColor = '#1E1E1E' - } return ( - - - - - + + {({ getRootProps, getInputProps }) => ( -
    -
    +
    +
    - - - - - Rename - - - - - - - - + } ) } + +export default LayerInstrument; \ No newline at end of file diff --git a/src/components/play/layer-settings/LayerListPopup.js b/src/components/play/layer-settings/LayerListPopup.js new file mode 100644 index 0000000..2613a98 --- /dev/null +++ b/src/components/play/layer-settings/LayerListPopup.js @@ -0,0 +1,90 @@ +import React, { useState, useEffect } from 'react' +import { Box, Typography, IconButton } from '@material-ui/core' + +import VolumeSlider from './VolumeSlider' +import _ from 'lodash' +import { CloseIcon } from './resources' + + +const LayerListPopup = ({ + classes, + showMixerPopup, + height, + selectedInstrument, + instrumentIcon, + onMuteClick, + onSoloClick, + toggleShowMixerPopup, + onLayerSelect, + ref, + userColors, + user, + round +}) => { + const [layers, setLayers] = useState([]) + + useEffect(() => { + if (round && round.layers) { + const newLayers = _.orderBy(round.layers, 'createdAt') + setLayers(newLayers) + } + }, [round]) + + return ( + + + + + Mixer + + + { + layers.map((layer, i) => + { + e.preventDefault(); + e.stopPropagation(); + onLayerSelect(layer.id) + }} + key={i} + className={classes.layerSubContainer} + > + + + + + {instrumentIcon(layer?.instrument?.sampler)} + + + {layer.instrument?.sample} + + + + + {userColors && layer && layer.createdBy && + + + } + + {layer.steps.length} + + + + {round && } + + + onSoloClick(layer)} className={classes.mixerButton}> + S + + onMuteClick(layer)} className={classes.mixerButton}> + M + + + + ) + } + + ) +} + +export default LayerListPopup \ No newline at end of file diff --git a/src/components/play/layer-settings/LayerNumberOfSteps.js b/src/components/play/layer-settings/LayerNumberOfSteps.js index 0e5cf6b..fd3c93f 100644 --- a/src/components/play/layer-settings/LayerNumberOfSteps.js +++ b/src/components/play/layer-settings/LayerNumberOfSteps.js @@ -28,7 +28,7 @@ const useStyles = makeStyles((theme) => ({ }, })); -export default function LayerNumberOfSteps ({ selectedLayer, user, roundId }) { +export default function LayerNumberOfSteps({ selectedLayer, user, roundId }) { const dispatch = useDispatch(); const classes = useStyles(); const firebase = useContext(FirebaseContext); diff --git a/src/components/play/layer-settings/LayerPercentOffset.js b/src/components/play/layer-settings/LayerPercentOffset.js index 513d7d2..44eb470 100644 --- a/src/components/play/layer-settings/LayerPercentOffset.js +++ b/src/components/play/layer-settings/LayerPercentOffset.js @@ -7,41 +7,108 @@ import Box from '@material-ui/core/Box'; import { makeStyles } from '@material-ui/core/styles'; import { FirebaseContext } from '../../../firebase'; import _ from 'lodash' -import { SET_LAYER_PERCENT_OFFSET } from '../../../redux/actionTypes' +import { + SET_LAYER_PERCENT_OFFSET, + SET_LAYER_TIME_OFFSET +} from '../../../redux/actionTypes' +import Percentage from './resources/svg/percentage.svg' +import { IconButton } from '@material-ui/core'; const useStyles = makeStyles((theme) => ({ root: { - width: '100%' + width: '100%', + margin: '0 0 20px 0' }, formControl: { margin: theme.spacing(1), - minWidth: 188, + minWidth: 50, [theme.breakpoints.down('sm')]: { minWidth: 100 }, }, + offsetDisplay: { + width: 88, + height: 48, + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + border: 'thin solid rgba(255, 255, 255, 0.1)', + alignItems: 'center', + marginBottom: 15, + padding: 10, + borderRadius: 5, + }, + switchButton: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + height: 32, + width: 32, + padding: 5, + borderRadius: 4, + '&:active': { + backgroundColor: 'rgba(255,255,255,0.1)', + } + }, selectEmpty: { marginTop: theme.spacing(2), }, })); -export default function LayerPercentOffset ({ selectedLayer, user, roundId, playUIRef }) { +export default function LayerPercentOffset({ + selectedLayer, + user, + horizontal, + roundId, + playUIRef, + offsetSliderRef, + percentageButtonRef, + msButtonRef, +}) { const dispatch = useDispatch(); const classes = useStyles(); const firebase = useContext(FirebaseContext); const [sliderValue, setSliderValue] = useState(selectedLayer.percentOffset || 0) + const [type, updateType] = useState('perc') + + useEffect(() => { + if (type === 'perc') { + setSliderValue(selectedLayer.percentOffset) + } else { + setSliderValue(selectedLayer.timeOffset) + } + }, [type, selectedLayer]) const updateLayerPercentOffsetState = (percent, selectedLayerId) => { dispatch({ type: SET_LAYER_PERCENT_OFFSET, payload: { id: selectedLayerId, value: percent } }) firebase.updateLayer(roundId, selectedLayer.id, { percentOffset: percent }) } + + const updateLayerTimeOffsetState = (ms, selectedLayerId) => { + dispatch({ type: SET_LAYER_TIME_OFFSET, payload: { id: selectedLayerId, value: ms } }) + firebase.updateLayer(roundId, selectedLayer.id, { timeOffset: ms }) + } + // eslint-disable-next-line react-hooks/exhaustive-deps const updateLayerPercentOffsetStateThrottled = useCallback(_.throttle(function (percent, selectedLayerId) { updateLayerPercentOffsetState(percent, selectedLayerId) }, 1000), []); - const onSliderChange = (e, percent) => { + // eslint-disable-next-line react-hooks/exhaustive-deps + const updateLayerTimeOffsetStateThrottled = useCallback(_.throttle(function (ms, selectedLayerId) { + updateLayerTimeOffsetState(ms, selectedLayerId) + }, 1000), []); + + const onSliderTimeChange = (e, ms) => { + setSliderValue(ms) + updateLayerTimeOffsetStateThrottled(ms, selectedLayer.id) + // Update UI directly for performance reasons (instead of going via redux) + playUIRef.adjustLayerOffset(selectedLayer.id, selectedLayer.percentOffset, ms) + } + + const onSliderPercChange = (e, percent) => { setSliderValue(percent) updateLayerPercentOffsetStateThrottled(percent, selectedLayer.id) // Update UI directly for performance reasons (instead of going via redux) @@ -52,15 +119,29 @@ export default function LayerPercentOffset ({ selectedLayer, user, roundId, play setSliderValue(selectedLayer.percentOffset || 0) // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectedLayer.id]) - return ( + const _onChange = (e, v) => { + if (type === 'perc') onSliderPercChange(e, v) + else if (type === 'ms') onSliderTimeChange(e, v) + } + return ( - - Time Offset (%) - - - - + + Time Offset + + {horizontal && + + updateType('perc')} className={classes.switchButton} style={type === 'perc' ? { backgroundColor: 'rgba(255,255,255,0.1)', } : {}}> + percentage + + updateType('ms')} className={classes.switchButton} style={type === 'ms' ? { backgroundColor: 'rgba(255,255,255,0.1)' } : {}}> + ms + + + } + + + ) } diff --git a/src/components/play/layer-settings/LayerPopup.js b/src/components/play/layer-settings/LayerPopup.js new file mode 100644 index 0000000..b949e69 --- /dev/null +++ b/src/components/play/layer-settings/LayerPopup.js @@ -0,0 +1,158 @@ +import React, { useContext } from 'react' +import { useDispatch } from "react-redux"; +import { Box, Typography } from '@material-ui/core' +import { withStyles } from '@material-ui/core/styles' +import LayerPercentOffset from './LayerPercentOffset' +import IconButton from '@material-ui/core/IconButton' + +import { SET_LAYER_STEPS } from '../../../redux/actionTypes' +import { FirebaseContext } from '../../../firebase'; +import { changeLayerLength } from '../../../utils/index' + +import Minus from './resources/svg/minus.svg' +import Plus from './resources/svg/plus.svg' + +const styles = theme => ({ + root: { + position: 'absolute', + display: "flex", + flexDirection: "column", + borderRadius: 8, + left: 0, + top: -253, + justifyContent: "flex-start", + alignItems: "center", + width: 155, + height: 257, + minHeight: 48, + backgroundColor: '#333333', + transition: 'opacity 0.2s ease-in', + boxShadow: '0px 0px 2px rgba(0, 0, 0, 0.15), 0px 4px 6px rgba(0, 0, 0, 0.15)', + zIndex: 100, + [theme.breakpoints.down('md')]: { + //height: 48, + }, + }, + offsetSlider: { + width: '100%', + padding: '5px 10px', + }, + stepCount: { + padding: '5px 10px', + borderRadius: 4, + backgroundColor: 'rgba(255, 255, 255, 0.1)' + }, + stepButtons: { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + width: 30, + height: 30 + }, + hidden: { + opacity: 0, + position: 'absolute', + top: '200%', + transition: 'opacity 0.2s ease-out' + }, + stepControls: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginTop: 10 + }, + stepsInput: { + textAlign: 'center', + color: 'white', + padding: 0, + margin: 0, + border: 'none', + width: 20, + outline: 'none', + backgroundColor: 'transparent' + } +}) + +const StepsDisplay = ({ + steps, + user, + round, + classes, + selectedLayer, + addStepsButtonRef, + subtractStepsButtonRef, +}) => { + const firebase = useContext(FirebaseContext) + const dispatch = useDispatch() + + const onNumberOfStepsChange = (steps) => { + const newSteps = changeLayerLength(selectedLayer, steps) + dispatch({ type: SET_LAYER_STEPS, payload: { id: selectedLayer.id, steps: newSteps, user: user.id } }) + firebase.updateLayer(round.id, selectedLayer.id, selectedLayer) + } + + const increaseSteps = () => onNumberOfStepsChange(steps + 1) + + const decreaseSteps = () => { + if (steps > 1) + onNumberOfStepsChange(steps - 1) + } + return ( + + Steps + + + less + + + + + + more + + + + ) +} + +const LayerPopup = ({ + classes, + round, + user, + showLayerPopup, + playUIRef, + selectedLayer, + addStepsButtonRef, + subtractStepsButtonRef, + offsetSliderRef, + percentageButtonRef, + msButtonRef, +}) => { + return ( + + + + + + + + + ) +} + +export default withStyles(styles)(LayerPopup) diff --git a/src/components/play/layer-settings/LayerSettings.js b/src/components/play/layer-settings/LayerSettings.js index a10b39e..62d2ff2 100644 --- a/src/components/play/layer-settings/LayerSettings.js +++ b/src/components/play/layer-settings/LayerSettings.js @@ -1,26 +1,88 @@ import React, { Component } from 'react' import { connect } from "react-redux"; -import { Divider, Drawer } from '@material-ui/core'; -import Button from '@material-ui/core/Button'; -import _ from 'lodash' -import { SET_LAYER_MUTE, REMOVE_LAYER, SET_IS_SHOWING_LAYER_SETTINGS, SET_LAYER_STEPS } from '../../../redux/actionTypes' +import { Typography } from '@material-ui/core'; +import IconButton from '@material-ui/core/IconButton'; +import _, { cloneDeep } from 'lodash' +import { + SET_LAYER_MUTE, + REMOVE_LAYER, + SET_IS_SHOWING_LAYER_SETTINGS, + SET_LAYER_STEPS, + SET_SELECTED_LAYER_ID, + ADD_LAYER +} from '../../../redux/actionTypes' import Box from '@material-ui/core/Box'; import { withStyles } from '@material-ui/core/styles'; -//import { convertPercentToDB, convertDBToPercent, numberRange } from '../../../utils/index' import AudioEngine from '../../../audio-engine/AudioEngine' +import Instruments from '../../../audio-engine/Instruments' -import VolumeSlider from './VolumeSlider' -import LayerInstrument from './LayerInstrument' -import LayerNumberOfSteps from './LayerNumberOfSteps' import { FirebaseContext } from '../../../firebase'; -import LayerType from './LayerType'; -import LayerAutomation from './LayerAutomation'; -import Track from '../../../audio-engine/Track' -import LayerPercentOffset from './LayerPercentOffset' -import LayerTimeOffset from './LayerTimeOffset' -import LayerCustomSounds from './LayerCustomSounds' - -const styles = theme => ({ +import LayerInstrument from './LayerInstrument' +import LayerPopup from './LayerPopup' +import VolumePopup from './VolumePopup' +import { getDefaultLayerData } from '../../../utils/defaultData'; +import LayerListPopup from './LayerListPopup'; +import HamburgerPopup from './HamburgerPopup'; +import DeleteClearPopup from './DeleteClearPopup'; +import { + PlusIcon, + EqualiserIcon, + HiHatsIcon, + KickIcon, + PercIcon, + SnareIcon, + MuteIcon, + MutedIcon, + ErasorIcon, + TrashIcon, + HamburgerMenuIcon, + CloseIcon, + ElipsisIcon, + CustomIcon +} from './resources' +import { + setIsShowingCustomInstrumentDialog, + updateCustomInstruments +} from '../../../redux/actions'; +import CustomInstrumentDialog from '../../dialogs/CustomInstrumentDialog'; + +const styles = (theme) => ({ + container: { + position: 'absolute', + display: 'flex', + justifyContent: 'center', + alignItems: 'flex-start', + backgroundColor: 'transparent', + bottom: 0, + right: '25%', + left: '25%', + [theme.breakpoints.down('md')]: { + right: '20%', + left: '20%' + }, + [theme.breakpoints.down('sm')]: { + right: '5%', + left: '5%' + }, + }, + addLayerMobile: { + display: 'none', + [theme.breakpoints.down('xs')]: { + display: 'flex' + }, + }, + addLayerDesktop: { + display: 'flex', + [theme.breakpoints.down('xs')]: { + display: 'none' + }, + }, + selectedInstrumentInfo: { + display: 'flex', + [theme.breakpoints.down('xs')]: { + display: 'none' + } + }, drawer: { backgroundColor: '#2E2E2E', '& .MuiPaper-root': { @@ -28,21 +90,324 @@ const styles = theme => ({ } }, root: { + boxSizing: 'border-box', + position: 'relative', display: "flex", - flexDirection: "column", - height: "100%", + flexDirection: "row", + height: 48, + borderRadius: 32, + marginBottom: 20, + justifyContent: "flex-start", alignItems: "center", - width: 300, - [theme.breakpoints.down('sm')]: { - width: 150 - }, + width: 547, /*'& > *': { marginBottom: '1rem' },*/ - paddingTop: theme.spacing(2), - backgroundColor: '#2E2E2E', - paddingLeft: theme.spacing(1), - paddingRight: theme.spacing(1), + backgroundColor: '#333333', + [theme.breakpoints.down('md')]: { + height: 48, + marginBottom: 10, + }, + [theme.breakpoints.down('sm')]: { + marginBottom: 5, + }, + [theme.breakpoints.down('xs')]: { + width: 341 + }, + }, + mixerPopup: { + display: 'flex', + flexDirection: 'column', + position: 'absolute', + opacity: 1, + top: -247, + height: 243, + width: 499, + right: 0, + left: 48, + borderRadius: 8, + zIndex: 100, + boxShadow: '0px 0px 2px rgba(0, 0, 0, 0.15), 0px 4px 6px rgba(0, 0, 0, 0.15)', + backgroundColor: '#333333', + overflow: 'hidden', + transition: 'opacity 0.2s ease-in', + [theme.breakpoints.down('sm')]: { + top: -163, + height: 160, + left: 0, + }, + [theme.breakpoints.down('xs')]: { + width: 341 + } + }, + mixerPopupHeader: { + display: 'flex', + flex: 1, + justifyContent: 'flex-start', + alignItems: 'center', + padding: '10px 15px', + borderBottom: 'thin solid rgba(255, 255, 255, 0.1)' + }, + mixerPopupHeaderText: { + marginLeft: 13, + fontSize: 18 + }, + buttonText: { lineHeight: 1, fontSize: 16 }, + instrumentPopup: { + position: 'absolute', + bottom: 47, + backgroundColor: '#333333', + right: 0, + left: 0, + borderRadius: 8, + padding: '5px 0', + zIndex: 100, + boxShadow: '0px 0px 2px rgba(0, 0, 0, 0.15), 0px 4px 6px rgba(0, 0, 0, 0.15)', + overflow: 'hidden', + transition: 'opacity 0.2s ease-in', + [theme.breakpoints.down('sm')]: { + width: 216, + }, + }, + instrumentSample: { + display: 'flex', + fontWeight: 'bolder', + lineHeight: 1, + textAlign: 'center', + textTransform: 'capitalize', + [theme.breakpoints.down('xs')]: { + flex: 1 + } + }, + addLayerContainer: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + padding: 0, + margin: 0, + backgroundColor: '#4D4D4D', + height: '100%', + borderRadius: 30 + }, + iconButtons: { + width: 48, + height: 48, + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.2)' + } + }, + rectButton: { + display: 'flex', + flexDirection: 'row', + padding: '5px 15px', + width: '100%', + borderRadius: 0 + }, + mixerButton: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(255, 255, 255, 0.1)', + height: 32, + width: 32, + marginLeft: 5, + marginRight: 5, + [theme.breakpoints.down('sm')]: { + width: 32, + height: 32, + }, + }, + volumeSliderContainer: { + display: 'flex', + flex: 2, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + instrumentIcon: { + width: 13.5, + height: 16 + }, + instrumentSummary: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', + margin: '10px 0', + width: 216, + height: 32, + fontWeight: 'bold', + padding: '6px 15px', + borderRadius: 24, + backgroundColor: 'rgba(255,255,255, 0.1)', + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.2)' + }, + [theme.breakpoints.down('xs')]: { + width: 106 + } + }, + stepCount: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + margin: '10px 0', + width: 59, + height: 32, + padding: '6px 12px', + borderRadius: 24, + backgroundColor: 'rgba(255,255,255, 0.1)', + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.2)' + }, + }, + stepLength: { + display: 'flex', + alignItems: 'flex-start', + lineHeight: 1, + }, + actionButtonContainer: { + position: 'relative', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + marginLeft: 8, + marginRight: 8 + // [theme.breakpoints.down('sm')]: { + // width: 106, + // }, + }, + hamburgerPopup: { + position: 'absolute', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + opacity: 1, + top: -108, + height: 104, + width: 155, + left: 0, + borderRadius: 8, + zIndex: 100, + boxShadow: '0px 0px 2px rgba(0, 0, 0, 0.15), 0px 4px 6px rgba(0, 0, 0, 0.15)', + backgroundColor: '#333333', + overflow: 'hidden', + transition: 'opacity 0.2s ease-in', + [theme.breakpoints.down('sm')]: { + left: 0, + }, + }, + deleteClearPopup: { + position: 'absolute', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + opacity: 1, + top: -100, + height: 104, + width: 155, + right: 0, + borderRadius: 8, + zIndex: 100, + boxShadow: '0px 0px 2px rgba(0, 0, 0, 0.15), 0px 4px 6px rgba(0, 0, 0, 0.15)', + backgroundColor: '#333333', + overflow: 'hidden', + transition: 'opacity 0.2s ease-in', + [theme.breakpoints.down('sm')]: { + right: 0, + }, + }, + desktopDeleteClear: { + display: 'flex', + [theme.breakpoints.down('xs')]: { + display: 'none' + } + }, + mobileDeleteClear: { + display: 'none', + [theme.breakpoints.down('xs')]: { + display: 'flex' + }, + }, + actionButton: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + padding: 5, + height: 32, + width: 32, + margin: '10px 0', + backgroundColor: 'rgba(255,255,255, 0.1)', + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.2)' + } + }, + msg: { + flex: 1, + textAlign: 'center', + padding: '0 15px', + [theme.breakpoints.down('xs')]: { + fontSize: 14, + padding: '0 15px' + } + }, + containerSoloMute: { + flex: 1, + display: 'flex', + paddingLeft: 20, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between' + }, + layerContainer: { + display: 'flex', + flex: 6, + overflowY: 'scroll', + height: '100%', + zIndex: 100, + flexDirection: 'column', + }, + layerSubContainer: { + flex: 1, + display: 'flex', + flexDirection: 'row', + cursor: 'pointer', + '&:hover': { + backgroundColor: 'rgba(255, 255, 255, 0.1)' + } + }, + layer: { + flex: 1, + display: 'flex', + flexDirection: 'row', + borderBottom: 'thin solid rgba(255, 255, 255, 0.1)', + paddingTop: 10, + paddingBottom: 10, + marginLeft: 20, + marginRight: 20 + }, + layerOptions: { + position: 'relative', + width: '90%', + display: 'flex', + justifyContent: 'flex-start', + alignItems: 'center' + }, + plainButton: { + '&:hover': { + backgroundColor: 'transparent' + } + }, + buttonWithText: { + display: 'flex', + flexDirection: 'row', + width: '100%', + height: 44, + alignItems: 'center', + borderRadius: 0, + justifyContent: 'space-between', }, buttonContainer: { width: '100%', @@ -66,34 +431,218 @@ const styles = theme => ({ width: '100%', marginTop: theme.spacing(2), marginBottom: theme.spacing(2), + }, + hidden: { + opacity: 0, + position: 'absolute', + top: '200%', + transition: 'opacity 0.2s ease-out' } }) class LayerSettings extends Component { + constructor(props) { + super(props) + this.state = { + showMixerPopup: false, + showInstrumentsPopup: false, + showInstrumentsList: false, + showSoundsList: false, + showArticulationOptions: false, + showLayerPopup: false, + showVolumePopup: false, + showHamburgerPopup: false, + showDeleteClearPopup: false, + windowWidth: 340, + isShowingCustomInstrumentDialog: false, + instrumentOptions: Instruments.getInstrumentOptions(false), + selectedInstrument: '' + } + this.addLayerButton = React.createRef() + this.mixerPopupButton = React.createRef() + this.instrumentPopupButton = React.createRef() + this.instrumentsListButton = React.createRef() + this.articulationsListButton = React.createRef() + this.showDeleteClearPopupButton = React.createRef() + this.layerPopupButton = React.createRef() + this.volumePopupButton = React.createRef() + + this.muteToggle = React.createRef() + this.soloButton = React.createRef() + this.offsetSlider = React.createRef() + this.volumeSlider = React.createRef() + + this.instrumentsButton = React.createRef() + this.soundsButton = React.createRef() + this.hamburgerButton = React.createRef() + this.addStepsButton = React.createRef() + this.subtractStepsButton = React.createRef() + this.percentageButton = React.createRef() + this.msButton = React.createRef() + this.height = window.innerHeight; + } + static contextType = FirebaseContext; - onCloseClick () { + resizeHeight = () => { + this.height = window.innerHeight; + } + componentDidMount() { + window.addEventListener('click', this.onClick) + window.addEventListener('resize', this.updateWindowWidth) + this.updateWindowWidth(); + window.addEventListener('resize', this.resizeHeight); + if (this.props.round && this.props.selectedLayerId) { + const selectedLayer = _.find(this.props.round.layers, { id: this.props.selectedLayerId }) + this.setSelectedInstrument(selectedLayer) + } + } + + componentDidUpdate(prevProps) { + if (this.props.round && this.props.selectedLayerId) { + const selectedLayer = _.find(this.props.round.layers, { id: this.props.selectedLayerId }) + if (selectedLayer && + ( + (prevProps.selectedLayerId !== this.props.selectedLayerId) || + (!this.state.selectedInstrument && selectedLayer) + ) + ) { + this.setSelectedInstrument(selectedLayer) + } + if (this.state.selectedInstrument) { + const selectedInstArray = this.state?.selectedInstrument.split(''); + const firstTwo = selectedInstArray[0] + selectedInstArray[1]; + selectedLayer?.instrument?.sampler.indexOf(firstTwo) === -1 && this.setSelectedInstrument(selectedLayer) + } + } + } + + componentWillUnmount() { + window.removeEventListener('click', this.onClick) + window.removeEventListener('resize', this.updateWindowWidth) + window.removeEventListener('resize', this.resizeHeight) + } + + updateWindowWidth = () => this.setState({ windowWidth: window.innerWidth }) + + setSelectedInstrument = async (selectedLayer) => { + const instrumentOptions = await Instruments.getInstrumentOptions() + if (instrumentOptions && this.props.round) { + const localLayer = _.find(this.props.round.layers, { id: this.props.selectedLayerId }) + const sampler = selectedLayer?.instrument?.sampler || localLayer?.instrument?.sampler; + const instrument = _.find(instrumentOptions, { name: sampler }) + if (instrument && this.state.selectedInstrument !== instrument.label) + this.setState({ selectedInstrument: instrument.label }) + } + } + + getUserColors() { + let userColors = {}; + for (const user of this.props.users) { + userColors[user.id] = user.color + } + return userColors + } + + onClick = (e) => { + e.preventDefault() + e.stopPropagation() + const target = e.target; + if (( + (!this.instrumentPopupButton.current || (this.instrumentPopupButton.current && !this.instrumentPopupButton.current.contains(target))) + //&& (!this.addLayerButton.current || (this.addLayerButton.current && !this.addLayerButton.current.contains(target))) + && (!this.articulationsListButton.current || (this.articulationsListButton.current && !this.articulationsListButton.current.contains(target))) + && (!this.hamburgerButton.current || (this.hamburgerButton.current && !this.hamburgerButton.current.contains(target))) + && (!this.showDeleteClearPopupButton.current || (this.showDeleteClearPopupButton.current && !this.showDeleteClearPopupButton.current.contains(target))) + && (!this.instrumentsListButton.current || (this.instrumentsListButton.current && !this.instrumentsListButton.current.contains(target))) + && (!this.instrumentsButton.current || (this.instrumentsButton.current && !this.instrumentsButton.current.contains(target))) + && (!this.soundsButton.current || (this.soundsButton.current && !this.soundsButton.current.contains(target))) + && (!this.addStepsButton.current || (this.addStepsButton.current && !this.addStepsButton.current.contains(target))) + && (!this.subtractStepsButton.current || (this.subtractStepsButton.current && !this.subtractStepsButton.current.contains(target))) + && (!this.percentageButton.current || (this.percentageButton.current && !this.percentageButton.current.contains(target))) + && (!this.msButton.current || (this.msButton.current && !this.msButton.current.contains(target))) + && (!this.layerPopupButton.current || (this.layerPopupButton.current && !this.layerPopupButton.current.contains(target))) + && (!this.mixerPopupButton.current || (this.mixerPopupButton.current && !this.mixerPopupButton.current.contains(target))) + && (!this.volumePopupButton.current || (this.volumePopupButton && !this.volumePopupButton.current.contains(target))) + && (!this.muteToggle.current || (this.muteToggle && !this.muteToggle.current.contains(target))) + && (!this.soloButton.current || (this.soloButton && !this.soloButton.current.contains(target))) + && (!this.volumeSlider.current || (this.volumeSlider && !this.volumeSlider.current.contains(target))) + && (!this.offsetSlider.current || (this.offsetSlider && !this.offsetSlider.current.contains(target))) + )) { + this.hideAllLayerInspectorModals() + } + } + + hideAllLayerInspectorModals = () => { + this.setState({ + showMixerPopup: false, + showInstrumentsPopup: false, + showInstrumentsList: false, + showSoundsList: false, + showArticulationOptions: false, + showLayerPopup: false, + showVolumePopup: false, + showDeleteClearPopup: false, + showHamburgerPopup: false + }) + } + + onCloseClick() { this.props.dispatch({ type: SET_IS_SHOWING_LAYER_SETTINGS, payload: { value: false } }) } - onPreviewClick () { - // todo: only audible to this user (mute for all others) + onLayerClicked = (layerId) => { + //this.selectedLayerId = layerId + this.props.dispatch({ type: SET_SELECTED_LAYER_ID, payload: { layerId } }) + //this.props.dispatch({ type: SET_IS_SHOWING_LAYER_SETTINGS, payload: { value: true } }) + //this.highlightLayer(_.find(this.layerGraphics, { id: layerId })) + } + + onPreviewClick() { + // TODO: only audible to this user (mute for all others) + } + + onSoloClick = async (selectedLayer) => { + const layers = this.props.round.layers + if (selectedLayer) { + await layers.forEach(layer => { + const id = layer.id; + const isMuted = !layer.isMuted; + if (selectedLayer.id !== id) { + AudioEngine.tracksById[id].setMute(isMuted) + this.props.dispatch({ type: SET_LAYER_MUTE, payload: { id, value: isMuted, user: this.props.user.id } }) + this.context.updateLayer(this.props.round.id, id, { isMuted }) + } + }); + } + } + + onMuteClick = (selectedLayer) => { + if (selectedLayer) { + const isMuted = !selectedLayer.isMuted + AudioEngine.tracksById[selectedLayer.id].setMute(isMuted) + this.props.dispatch({ type: SET_LAYER_MUTE, payload: { id: selectedLayer.id, value: isMuted, user: this.props.user.id } }) + this.context.updateLayer(this.props.round.id, selectedLayer.id, { isMuted }) + } } - onMuteClick () { - const isMuted = !this.props.selectedLayer.isMuted - AudioEngine.tracksById[this.props.selectedLayer.id].setMute(isMuted) - this.props.dispatch({ type: SET_LAYER_MUTE, payload: { id: this.props.selectedLayer.id, value: isMuted, user: this.props.user.id } }) - this.context.updateLayer(this.props.round.id, this.props.selectedLayer.id, { isMuted }) + onDeleteLayerClick() { + const selectedLayer = this.props.selectedLayer + if (selectedLayer) { + this.props.dispatch({ type: REMOVE_LAYER, payload: { id: selectedLayer.id, user: this.props.user.id } }) + this.context.deleteLayer(this.props.round.id, selectedLayer.id) + this.onCloseClick() + } } - onDeleteLayerClick () { - this.props.dispatch({ type: REMOVE_LAYER, payload: { id: this.props.selectedLayer.id, user: this.props.user.id } }) - this.context.deleteLayer(this.props.round.id, this.props.selectedLayer.id) - this.onCloseClick() + onAddLayerClick = async () => { + const newLayer = await getDefaultLayerData(this.props.user.id); + newLayer.name = 'Layer ' + (this.props.round.layers.length + 1) + this.props.dispatch({ type: ADD_LAYER, payload: { layer: newLayer, user: this.props.user.id } }) + this.context.createLayer(this.props.round.id, newLayer) } - onClearStepsClick () { + onClearStepsClick() { let selectedLayerClone = _.cloneDeep(this.props.selectedLayer) for (let step of selectedLayerClone.steps) { step.isOn = false @@ -102,69 +651,329 @@ class LayerSettings extends Component { this.context.updateLayer(this.props.round.id, selectedLayerClone.id, { steps: selectedLayerClone.steps }) } + toggleInstrumentPopup = (e) => { + e.preventDefault() + e.stopPropagation() + const showInstrumentsPopup = !this.state.showInstrumentsPopup + this.hideAllLayerInspectorModals() + this.setState({ showInstrumentsPopup }) + } + + toggleShowInstrumentList = (e) => { + e.preventDefault() + e.stopPropagation() + const showInstrumentsList = !this.state.showInstrumentsList + this.hideAllLayerInspectorModals() + this.setState({ showInstrumentsList, showInstrumentsPopup: true }) + } + + toggleArticulationOptions = (e) => { + e.preventDefault() + e.stopPropagation() + const showArticulationOptions = !this.state.showArticulationOptions + this.hideAllLayerInspectorModals() + this.setState({ showArticulationOptions, showInstrumentsPopup: true }) + } + + toggleLayerPopup = (e) => { + e.preventDefault() + e.stopPropagation() + const showLayerPopup = !this.state.showLayerPopup + this.hideAllLayerInspectorModals() + this.setState({ showLayerPopup }) + } + + toggleVolumePopup = (e) => { + e.preventDefault() + e.stopPropagation() + const showVolumePopup = !this.state.showVolumePopup + this.hideAllLayerInspectorModals() + this.setState({ showVolumePopup }) + } + + toggleShowMixerPopup = (e) => { + e.preventDefault() + e.stopPropagation() + const showMixerPopup = !this.state.showMixerPopup + this.hideAllLayerInspectorModals() + this.setState({ showMixerPopup }) + } + + toggleShowHamburgerPop = (e) => { + e.preventDefault() + e.stopPropagation() + const showHamburgerPopup = !this.state.showHamburgerPopup + this.hideAllLayerInspectorModals() + this.setState({ showHamburgerPopup }) + } + + toggleShowDeleteClearPopup = (e) => { + e.preventDefault() + e.stopPropagation() + const showDeleteClearPopup = !this.state.showDeleteClearPopup + this.hideAllLayerInspectorModals() + this.setState({ showDeleteClearPopup }) + } + + toggleShowCustomInstrumentDialog = (val) => { + const { isShowingCustomInstrumentDialog, setIsShowingCustomInstrumentDialog } = this.props + const newShowing = !isShowingCustomInstrumentDialog + if (val === undefined) + setIsShowingCustomInstrumentDialog(newShowing) + else setIsShowingCustomInstrumentDialog(val) + } + + addInstrumentToRound = (samples) => { + const { round, user, updateCustomInstruments } = this.props + const customInstruments = round?.customInstruments ? cloneDeep(round.customInstruments) : {} + samples.forEach(sample => { + customInstruments[sample.id] = sample + }) + this.context.updateCustomInstruments(round.id, user.id, customInstruments) + updateCustomInstruments(customInstruments) + } + + render() { + const { + showMixerPopup, + showInstrumentsPopup, + showInstrumentsList, + showArticulationOptions, + showLayerPopup, + showHamburgerPopup, + selectedInstrument, + showVolumePopup, + showDeleteClearPopup, + windowWidth + } = this.state - render () { - // console.log('Layer settings render()', this.props.user); - const { classes } = this.props + const { classes, theme, user, round } = this.props const selectedLayer = this.props.selectedLayer - let form = ''; - if (!_.isNil(selectedLayer)) { - //layerVolumePercent = convertDBToPercent(selectedLayer.instrument.gain) - let layerTypeFormItems; - if (selectedLayer.type === Track.TRACK_TYPE_AUTOMATION) { - layerTypeFormItems = ( - <> - - - - - - - ) - } else { - layerTypeFormItems = ( - <> - - - - - - - - - - - ) - } - form = ( - - - - - - {layerTypeFormItems} - - ) + const userColors = this.getUserColors() + const isMobile = windowWidth < theme.breakpoints.values.sm + let sample = selectedLayer?.instrument?.sample + + if (typeof sample === 'object') { + sample = selectedLayer?.instrument?.sampler } - return ( -
    - + const instrumentIcon = (name) => { + let Icon = CustomIcon; + if (name === 'HiHats') + Icon = HiHatsIcon + if (name === 'Kicks') + Icon = KickIcon + if (name === 'Snares') + Icon = SnareIcon + if (name === 'Perc') + Icon = PercIcon + return + + + } - {form} + const form = ( + + + + + + + + + + + + + + + + {showHamburgerPopup ? + : + } + + + + + {!selectedLayer && + + Long Press a round to edit + } + {selectedLayer && + + + + + + {instrumentIcon(selectedLayer?.instrument?.sampler)} + + + + {selectedInstrument === 'Custom' ? selectedLayer.instrument.displayName : selectedInstrument} + + · + + + {`${sample.substring(0, isMobile ? 6 : sample.length)}${isMobile && + sample.length > 6 ? '...' : ''}`} + + + + + + + + + + + + {selectedLayer.steps.length} + + + + + + {selectedLayer.isMuted ? : } + + + + + + + + + + + + + + + + + + + + + + + } + + + ) - + return ( +
    + {user && user.id && form}
    ) } } const mapStateToProps = state => { - // console.log('mapStateToProps', state); let selectedLayer = null; if (!_.isNil(state.display.selectedLayerId) && !_.isNil(state.round) && !_.isNil(state.round.layers)) { selectedLayer = _.find(state.round.layers, { id: state.display.selectedLayerId }) @@ -172,12 +981,22 @@ const mapStateToProps = state => { return { round: state.round, user: state.user, + users: state.users, + selectedLayerId: state.display.selectedLayerId, selectedLayer, - isOpen: state.display.isShowingLayerSettings + isOpen: state.display.isShowingLayerSettings, + isShowingCustomInstrumentDialog: state.display.isShowingCustomInstrumentDialog, }; }; +const mapDispatchToProps = dispatch => ({ + setIsShowingCustomInstrumentDialog: val => dispatch(setIsShowingCustomInstrumentDialog(val)), + updateCustomInstruments: val => dispatch(updateCustomInstruments(val)), + dispatch +}) + export default connect( - mapStateToProps -)(withStyles(styles)(LayerSettings)) \ No newline at end of file + mapStateToProps, + mapDispatchToProps +)(withStyles(styles, { withTheme: true })(LayerSettings)) \ No newline at end of file diff --git a/src/components/play/layer-settings/LayerTimeOffset.js b/src/components/play/layer-settings/LayerTimeOffset.js index 546ce27..e722e69 100644 --- a/src/components/play/layer-settings/LayerTimeOffset.js +++ b/src/components/play/layer-settings/LayerTimeOffset.js @@ -26,7 +26,7 @@ const useStyles = makeStyles((theme) => ({ }, })); -export default function LayerTimeOffset ({ selectedLayer, user, roundId, playUIRef }) { +export default function LayerTimeOffset({ selectedLayer, user, roundId, playUIRef }) { const dispatch = useDispatch(); const classes = useStyles(); const firebase = useContext(FirebaseContext); @@ -57,7 +57,7 @@ export default function LayerTimeOffset ({ selectedLayer, user, roundId, playUIR Time Offset (ms) - + diff --git a/src/components/play/layer-settings/VolumePopup.js b/src/components/play/layer-settings/VolumePopup.js new file mode 100644 index 0000000..024202a --- /dev/null +++ b/src/components/play/layer-settings/VolumePopup.js @@ -0,0 +1,120 @@ +import React from 'react' +import { Box, Typography } from '@material-ui/core' +import { withStyles } from '@material-ui/core/styles' +import VolumeSlider from './VolumeSlider' +import IconButton from '@material-ui/core/IconButton' + +const styles = theme => ({ + root: { + position: 'absolute', + display: "flex", + flexDirection: "row", + borderRadius: 8, + width: 236, + height: 64, + right: -100, + top: -60, + justifyContent: "flex-start", + alignItems: "center", + backgroundColor: '#333333', + transition: 'opacity 0.2s ease-in', + boxShadow: '0px 0px 2px rgba(0, 0, 0, 0.15), 0px 4px 6px rgba(0, 0, 0, 0.15)', + zIndex: 100, + [theme.breakpoints.down('xs')]: { + right: -55, + }, + }, + offsetSlider: { + width: '100%', + padding: 10, + }, + stepCount: { + padding: 10, + borderRadius: 4, + backgroundColor: 'rgba(255, 255, 255, 0.1)' + }, + stepButtons: { + backgroundColor: 'rgba(255, 255, 255, 0.1)', + width: 30, + height: 30 + }, + hidden: { + opacity: 0, + position: 'absolute', + top: '200%', + transition: 'opacity 0.2s ease-out' + }, + stepControls: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginTop: 10 + }, + mixerButton: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: 'rgba(255, 255, 255, 0.1)', + marginLeft: 8, + marginRight: 8, + height: 30, + width: 30, + [theme.breakpoints.down('sm')]: { + width: 32, + height: 32, + }, + }, + containerSoloMute: { + flex: 1, + display: 'flex', + marginLeft: 8, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between' + }, + volumeSliderContainer: { + flex: 2, + marginLeft: 8, + flexDirection: 'row', + alignItems: 'center' + }, +}) + +const VolumePopup = ({ + classes, + round, + user, + showVolumePopup, + volumeSliderRef, + muteRef, + soloRef, + selectedLayer, + onMute, + onSolo +}) => { + return ( + + + + + + onSolo(selectedLayer)} className={classes.mixerButton}> + S + + onMute(selectedLayer)} className={classes.mixerButton}> + M + + + + ) +} + +export default withStyles(styles)(VolumePopup) diff --git a/src/components/play/layer-settings/VolumeSlider.js b/src/components/play/layer-settings/VolumeSlider.js index 0715142..8a3888a 100644 --- a/src/components/play/layer-settings/VolumeSlider.js +++ b/src/components/play/layer-settings/VolumeSlider.js @@ -13,16 +13,20 @@ import { Box } from '@material-ui/core'; const styles = makeStyles(function (theme) { return { root: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', width: '100%', padding: theme.spacing(1) }, slider: { + minWidth: 108, width: '100%' } } }) -export default function VolumeSlider ({ selectedLayer, user, roundId }) { +export default function VolumeSlider({ selectedLayer, sliderRef, user, roundId, hideText }) { const dispatch = useDispatch(); const firebase = useContext(FirebaseContext); const [sliderValue, setSliderValue] = useState(80) @@ -35,6 +39,8 @@ export default function VolumeSlider ({ selectedLayer, user, roundId }) { updateVolumeState(dB, selectedLayerId) }, 2000), []); const onSliderChange = (e, percent) => { + e.preventDefault() + e.stopPropagation() setSliderValue(percent) const dB = convertPercentToDB(percent) AudioEngine.tracksById[selectedLayer.id].setVolume(dB) @@ -42,46 +48,18 @@ export default function VolumeSlider ({ selectedLayer, user, roundId }) { } useEffect(() => { - //console.log('selectedLayer.id changed', selectedLayer.id, selectedLayer.instrument.gain); setSliderValue(convertDBToPercent(selectedLayer.gain)) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedLayer.id]) - - /* const verticalSliderMarks = [ - { - value: 100, - label: '+6', - }, - { - value: 80, - label: '0', - }, - { - value: 60, - label: '-6', - }, - - { - value: 34, - label: '-24', - }, - - { - value: 0, - label: '-96', - } - ];*/ + }, [selectedLayer.id, selectedLayer.gain]) const classes = styles() - //console.log('rendering volume slider', selectedLayer.id); return ( - - Volume + {!hideText && Volume} + + + + ) +} \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/BackButton.js b/src/components/play/layer-settings/resources/BackButton.js new file mode 100644 index 0000000..578bc09 --- /dev/null +++ b/src/components/play/layer-settings/resources/BackButton.js @@ -0,0 +1,9 @@ +import React from 'react' + +export default function BackButton({ fill }) { + return ( + + + + ) +} diff --git a/src/components/play/layer-settings/resources/Close.js b/src/components/play/layer-settings/resources/Close.js new file mode 100644 index 0000000..51dd4b5 --- /dev/null +++ b/src/components/play/layer-settings/resources/Close.js @@ -0,0 +1,11 @@ +import React from 'react' + +export default function Close({ + fill +}) { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/Copy.js b/src/components/play/layer-settings/resources/Copy.js new file mode 100644 index 0000000..ad422da --- /dev/null +++ b/src/components/play/layer-settings/resources/Copy.js @@ -0,0 +1,12 @@ +import React from 'react' + +export default function Copy({ + fill +}) { + return ( + + + + + ) +} \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/Custom.js b/src/components/play/layer-settings/resources/Custom.js new file mode 100644 index 0000000..d9ff144 --- /dev/null +++ b/src/components/play/layer-settings/resources/Custom.js @@ -0,0 +1,12 @@ +import React from 'react' + +export default function Custom({ + fill +}) { + return ( + + + + + ) +} \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/Di.js b/src/components/play/layer-settings/resources/Di.js new file mode 100644 index 0000000..fa5e238 --- /dev/null +++ b/src/components/play/layer-settings/resources/Di.js @@ -0,0 +1,14 @@ +import React from 'react' + +export default function Di({ + fill +}) { + return ( + + + + + + + ) +} \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/Elipsis.js b/src/components/play/layer-settings/resources/Elipsis.js new file mode 100644 index 0000000..8ad745a --- /dev/null +++ b/src/components/play/layer-settings/resources/Elipsis.js @@ -0,0 +1,15 @@ +import React from 'react' + +export default function Elipsis({ + fill, + height, + width +}) { + return ( + + + + + + ) +} diff --git a/src/components/play/layer-settings/resources/Equaliser.js b/src/components/play/layer-settings/resources/Equaliser.js new file mode 100644 index 0000000..125073c --- /dev/null +++ b/src/components/play/layer-settings/resources/Equaliser.js @@ -0,0 +1,16 @@ +import React from 'react' + +export default function Equaliser({ + user, + userColors, + height, + width +}) { + return ( + + + + + + ) +} diff --git a/src/components/play/layer-settings/resources/Erasor.js b/src/components/play/layer-settings/resources/Erasor.js new file mode 100644 index 0000000..da89ae8 --- /dev/null +++ b/src/components/play/layer-settings/resources/Erasor.js @@ -0,0 +1,11 @@ +import React from 'react' + +export default function Erasor({ + fill +}) { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/Hamburger.js b/src/components/play/layer-settings/resources/Hamburger.js new file mode 100644 index 0000000..8079585 --- /dev/null +++ b/src/components/play/layer-settings/resources/Hamburger.js @@ -0,0 +1,12 @@ +import React from 'react' + +export default function Hamburger({ + user, + userColors +}) { + return ( + + + + ) +} diff --git a/src/components/play/layer-settings/resources/HiHats.js b/src/components/play/layer-settings/resources/HiHats.js new file mode 100644 index 0000000..3ab0e72 --- /dev/null +++ b/src/components/play/layer-settings/resources/HiHats.js @@ -0,0 +1,11 @@ +import React from 'react' + +export default function HiHats({ + fill +}) { + return ( + + + + ) +} diff --git a/src/components/play/layer-settings/resources/Kick.js b/src/components/play/layer-settings/resources/Kick.js new file mode 100644 index 0000000..d815f59 --- /dev/null +++ b/src/components/play/layer-settings/resources/Kick.js @@ -0,0 +1,11 @@ +import React from 'react' + +export default function Kick({ + fill +}) { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/Mute.js b/src/components/play/layer-settings/resources/Mute.js new file mode 100644 index 0000000..1355b2d --- /dev/null +++ b/src/components/play/layer-settings/resources/Mute.js @@ -0,0 +1,13 @@ +import React from 'react' + +export default function Mute({ + fill +}) { + return ( + + + + + + ) +} \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/Muted.js b/src/components/play/layer-settings/resources/Muted.js new file mode 100644 index 0000000..bf5a678 --- /dev/null +++ b/src/components/play/layer-settings/resources/Muted.js @@ -0,0 +1,12 @@ +import React from 'react' + +export default function Muted({ + fill +}) { + return ( + + + + + ) +} \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/Perc.js b/src/components/play/layer-settings/resources/Perc.js new file mode 100644 index 0000000..d82817d --- /dev/null +++ b/src/components/play/layer-settings/resources/Perc.js @@ -0,0 +1,11 @@ +import React from 'react' + +export default function Perc({ + fill +}) { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/Playback.js b/src/components/play/layer-settings/resources/Playback.js new file mode 100644 index 0000000..a34677c --- /dev/null +++ b/src/components/play/layer-settings/resources/Playback.js @@ -0,0 +1,9 @@ +import React from 'react' + +export default function Playback({ fill }) { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/Plus.js b/src/components/play/layer-settings/resources/Plus.js new file mode 100644 index 0000000..bfee8ca --- /dev/null +++ b/src/components/play/layer-settings/resources/Plus.js @@ -0,0 +1,14 @@ +import React from 'react' + +export default function Plus({ + user, + userColors, + width, + height +}) { + return ( + + + + ) +} diff --git a/src/components/play/layer-settings/resources/Snare.js b/src/components/play/layer-settings/resources/Snare.js new file mode 100644 index 0000000..3a8428b --- /dev/null +++ b/src/components/play/layer-settings/resources/Snare.js @@ -0,0 +1,12 @@ +import React from 'react' + +export default function Snare({ + fill +}) { + return ( + + + + + ) +} \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/Trash.js b/src/components/play/layer-settings/resources/Trash.js new file mode 100644 index 0000000..eb4d42d --- /dev/null +++ b/src/components/play/layer-settings/resources/Trash.js @@ -0,0 +1,11 @@ +import React from 'react' + +export default function Trash({ + fill +}) { + return ( + + + + ) +} \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/Upload.js b/src/components/play/layer-settings/resources/Upload.js new file mode 100644 index 0000000..f00cef4 --- /dev/null +++ b/src/components/play/layer-settings/resources/Upload.js @@ -0,0 +1,12 @@ +import React from 'react' + +export default function Upload({ + fill +}) { + return ( + + + + + ) +} \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/index.js b/src/components/play/layer-settings/resources/index.js new file mode 100644 index 0000000..e901068 --- /dev/null +++ b/src/components/play/layer-settings/resources/index.js @@ -0,0 +1,15 @@ +export { default as PlusIcon } from './Plus' +export { default as EqualiserIcon } from './Equaliser' +export { default as HiHatsIcon } from './HiHats' +export { default as HamburgerMenuIcon } from './Hamburger' +export { default as KickIcon } from './Kick' +export { default as SnareIcon } from './Snare' +export { default as PercIcon } from './Perc' +export { default as CloseIcon } from './Close' +export { default as MuteIcon } from './Mute' +export { default as MutedIcon } from './Muted' +export { default as ErasorIcon } from './Erasor' +export { default as TrashIcon } from './Trash' +export { default as ElipsisIcon } from './Elipsis' +export { default as BackButton } from './BackButton' +export { default as CustomIcon } from './Custom' diff --git a/src/components/play/layer-settings/resources/svg/add.svg b/src/components/play/layer-settings/resources/svg/add.svg new file mode 100644 index 0000000..b86f4cb --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/add.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/check.svg b/src/components/play/layer-settings/resources/svg/check.svg new file mode 100644 index 0000000..e274f38 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/check.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/close.svg b/src/components/play/layer-settings/resources/svg/close.svg new file mode 100644 index 0000000..6aae47a --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/close.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/copy.svg b/src/components/play/layer-settings/resources/svg/copy.svg new file mode 100644 index 0000000..f5d42dc --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/copy.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/custom.svg b/src/components/play/layer-settings/resources/svg/custom.svg new file mode 100644 index 0000000..6f0fe6e --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/custom.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/di.svg b/src/components/play/layer-settings/resources/svg/di.svg new file mode 100644 index 0000000..12fa1e0 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/di.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/erasor.svg b/src/components/play/layer-settings/resources/svg/erasor.svg new file mode 100644 index 0000000..c41e2bd --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/erasor.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/hihat.svg b/src/components/play/layer-settings/resources/svg/hihat.svg new file mode 100644 index 0000000..b1a5730 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/hihat.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/instruments.svg b/src/components/play/layer-settings/resources/svg/instruments.svg new file mode 100644 index 0000000..3bacd3c --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/instruments.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/kick.svg b/src/components/play/layer-settings/resources/svg/kick.svg new file mode 100644 index 0000000..f9d8689 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/kick.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/layer.svg b/src/components/play/layer-settings/resources/svg/layer.svg new file mode 100644 index 0000000..dad8af9 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/layer.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/leftArrow.svg b/src/components/play/layer-settings/resources/svg/leftArrow.svg new file mode 100644 index 0000000..46ec207 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/leftArrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/lock.svg b/src/components/play/layer-settings/resources/svg/lock.svg new file mode 100644 index 0000000..73040d0 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/lock.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/minus.svg b/src/components/play/layer-settings/resources/svg/minus.svg new file mode 100644 index 0000000..bcb6bdb --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/minus.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/muted.svg b/src/components/play/layer-settings/resources/svg/muted.svg new file mode 100644 index 0000000..0e019fe --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/muted.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/openLock.svg b/src/components/play/layer-settings/resources/svg/openLock.svg new file mode 100644 index 0000000..9b308d3 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/openLock.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/pause.svg b/src/components/play/layer-settings/resources/svg/pause.svg new file mode 100644 index 0000000..dc58072 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/pause.svg @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/perc.svg b/src/components/play/layer-settings/resources/svg/perc.svg new file mode 100644 index 0000000..ca906d9 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/perc.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/percentage.svg b/src/components/play/layer-settings/resources/svg/percentage.svg new file mode 100644 index 0000000..f448107 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/percentage.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/play.svg b/src/components/play/layer-settings/resources/svg/play.svg new file mode 100644 index 0000000..222e408 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/play.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/playback.svg b/src/components/play/layer-settings/resources/svg/playback.svg new file mode 100644 index 0000000..e8526a2 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/playback.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/plus.svg b/src/components/play/layer-settings/resources/svg/plus.svg new file mode 100644 index 0000000..d22cbb3 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/plus.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/rightArrow.svg b/src/components/play/layer-settings/resources/svg/rightArrow.svg new file mode 100644 index 0000000..02dea81 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/rightArrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/roundAdd.svg b/src/components/play/layer-settings/resources/svg/roundAdd.svg new file mode 100644 index 0000000..c5e8d73 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/roundAdd.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/snare.svg b/src/components/play/layer-settings/resources/svg/snare.svg new file mode 100644 index 0000000..bf05e88 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/snare.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/trash.svg b/src/components/play/layer-settings/resources/svg/trash.svg new file mode 100644 index 0000000..6f39acf --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/trash.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/upload.svg b/src/components/play/layer-settings/resources/svg/upload.svg new file mode 100644 index 0000000..6fbb793 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/upload.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/components/play/layer-settings/resources/svg/volume.svg b/src/components/play/layer-settings/resources/svg/volume.svg new file mode 100644 index 0000000..69696f8 --- /dev/null +++ b/src/components/play/layer-settings/resources/svg/volume.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/components/rounds-list-route/RoundsListRoute.js b/src/components/rounds-list-route/RoundsListRoute.js index 28fb380..30130e4 100644 --- a/src/components/rounds-list-route/RoundsListRoute.js +++ b/src/components/rounds-list-route/RoundsListRoute.js @@ -12,12 +12,18 @@ import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; import ListItemAvatar from '@material-ui/core/ListItemAvatar'; import Avatar from '@material-ui/core/Avatar'; import ImageIcon from '@material-ui/icons/Image'; -import AddIcon from '@material-ui/icons/Add'; +//import AddIcon from '@material-ui/icons/Add'; import MoreHorizIcon from '@material-ui/icons/MoreHoriz'; import { connect } from "react-redux"; import _ from 'lodash'; import { - setIsShowingSignInDialog, setRedirectAfterSignIn, setRounds, setIsShowingDeleteRoundDialog, setIsShowingRenameDialog, setSelectedRoundId + setIsShowingSignInDialog, + setIsShowingCreateRoundDialog, + setRedirectAfterSignIn, + setRounds, + setIsShowingDeleteRoundDialog, + setIsShowingRenameDialog, + setSelectedRoundId } from '../../redux/actions' import SignInDialog from '../dialogs/SignInDialog' import { createRound } from '../../utils/index' @@ -29,11 +35,15 @@ import Popper from '@material-ui/core/Popper'; import MenuItem from '@material-ui/core/MenuItem'; import MenuList from '@material-ui/core/MenuList'; import { uuid } from '../../utils/index' +import CreateRoundDialog from '../dialogs/CreateRoundDialog'; +import CustomSamples from '../../audio-engine/CustomSamples'; const styles = theme => ({ root: { paddingTop: '64px' - + }, + paper: { + borderRadius: 8 }, header: { display: 'flex', @@ -45,7 +55,7 @@ const styles = theme => ({ class RoundsListRoute extends Component { static contextType = FirebaseContext; - constructor (props) { + constructor(props) { super(props); this.state = { menuIsOpen: false, @@ -56,22 +66,33 @@ class RoundsListRoute extends Component { this.onMenuClick = this.onMenuClick.bind(this) } - async onNewRoundClick () { - console.log('create new round'); - let newRound = createRound(this.props.user.id) - console.log('newRound', newRound); - let newRounds = [...this.props.rounds, newRound] + componentDidMount() { + CustomSamples.init(this.context) + } + + async onNewRoundClick(callback, sounds) { + let newRound = await createRound(this.props.user.id, sounds) + let newRounds = [newRound, ...this.props.rounds] await this.context.createRound(newRound) - this.props.setRounds(newRounds) + await this.props.setRounds(newRounds) // redirect to new round this.onLaunchRoundClick(newRound.id) + callback && callback() } - onLaunchRoundClick (id) { + toggleCreateRoundDialog = (val) => { + const { isShowingCreateRoundDialog, setIsShowingCreateRoundDialog } = this.props + const newShowing = !isShowingCreateRoundDialog + if (val === undefined) + setIsShowingCreateRoundDialog(newShowing) + else setIsShowingCreateRoundDialog(val) + } + + onLaunchRoundClick(id) { this.props.history.push('/play/' + id) } - getCreatedString (round) { + getCreatedString(round) { const date = new Date(round.createdAt) let dateString = date.toLocaleTimeString( 'en-gb', @@ -84,9 +105,8 @@ class RoundsListRoute extends Component { return dateString } - onMenuClick (roundId, e) { + onMenuClick(roundId, e) { let element = e.currentTarget - console.log('onMenuClick', roundId, element); this.props.setSelectedRoundId(roundId) this.setState({ anchorElement: element, @@ -108,13 +128,12 @@ class RoundsListRoute extends Component { this.props.setIsShowingRenameDialog(true) } onDuplicateClick = async () => { - console.log('onDuplicateClick'); let selectedRound = await this.context.getRound(this.props.selectedRoundId) let clonedRound = _.cloneDeep(selectedRound) clonedRound.id = uuid() clonedRound.name += ' (duplicate)' clonedRound.createdAt = Date.now() - this.context.createRound(clonedRound) + await this.context.createRound(clonedRound) let clonedRounds = _.cloneDeep(this.props.rounds) clonedRounds.push(clonedRound) this.props.setRounds(clonedRounds) @@ -129,26 +148,27 @@ class RoundsListRoute extends Component { this.props.setIsShowingDeleteRoundDialog(true) } - render () { - console.log('rendering rounds', this.props.rounds); - + render() { const { classes } = this.props; - const rounds = this.props.rounds; + const rounds = [...this.props.rounds]; return ( <> -

    My rounds

    -
    - -
    + +

    My rounds

    +
    + + {/* */} + +
    { rounds.map((round) => ( - + @@ -174,14 +194,13 @@ class RoundsListRoute extends Component { {...TransitionProps} style={{ transformOrigin: placement === 'bottom' ? 'center top' : 'center bottom' }} > - + Rename Duplicate Delete - @@ -191,6 +210,12 @@ class RoundsListRoute extends Component {
    + { + this.onNewRoundClick(callback, sounds) + }} + /> ) } @@ -203,6 +228,7 @@ const mapStateToProps = state => { return { user: state.user, rounds: state.rounds, + isShowingCreateRoundDialog: state.display.isShowingCreateRoundDialog, selectedRoundId: state.display.selectedRoundId }; }; @@ -213,6 +239,7 @@ export default connect( setIsShowingSignInDialog, setRedirectAfterSignIn, setRounds, + setIsShowingCreateRoundDialog, setIsShowingDeleteRoundDialog, setIsShowingRenameDialog, setSelectedRoundId diff --git a/src/firebase/firebase.js b/src/firebase/firebase.js index d5eaa86..89f3a54 100644 --- a/src/firebase/firebase.js +++ b/src/firebase/firebase.js @@ -17,7 +17,7 @@ var firebaseConfig = { }; class Firebase { - constructor () { + constructor() { if (!firebase.apps.length) { app.initializeApp(firebaseConfig); } @@ -35,7 +35,6 @@ class Firebase { this.onUserUpdatedObservers = []; app.auth().onAuthStateChanged((user) => { - // console.log('onAuthStateChanged', user); if (user) { this.currentUser = user; this.onUserUpdatedObservers.map(observer => observer(user)); @@ -47,6 +46,29 @@ class Firebase { }); } + uploadSound = (id, sounds) => { + return new Promise(async (resolve, reject) => { + try { + const storageRef = this.storage.ref() + const currentUserStorageRef = storageRef.child(`/${id}`) + const fileURLs = [] + if (id && sounds && Array.isArray(sounds)) { + sounds.forEach(async sound => { + await currentUserStorageRef.child(sound.name).put(sound.file).then(async (uploadResponse) => { + const url = await uploadResponse.ref.getDownloadURL() + fileURLs.push(url) + if (fileURLs.length === sounds.length) { + resolve(fileURLs) + } + }) + }) + } + else reject({ message: 'missing required data' }) + } catch (e) { + console.error(e) + } + }) + } // User loadUser = (id) => { @@ -114,6 +136,8 @@ class Firebase { const roundsSnapshot = await this.db .collection("rounds") .where('createdBy', '==', userId) + .orderBy('createdAt', 'desc') + .limitToLast() .get(); roundsSnapshot.forEach(roundDoc => { let round = roundDoc.data(); @@ -140,6 +164,7 @@ class Firebase { round.layers = await this.getLayers(roundId) round.userBuses = await this.getUserBuses(roundId) round.userPatterns = await this.getUserPatterns(roundId) + round.customInstruments = await this.getCustomInstruments(roundId) // console.log('got round', round); resolve(round) } catch (e) { @@ -159,7 +184,7 @@ class Firebase { .get(); layerSnapshot.forEach(layerDoc => { let layer = layerDoc.data(); - layer.id = layerDoc.id; + //layer.id = layerDoc.id; layers.push(layer); }) @@ -172,6 +197,29 @@ class Firebase { }) } + getCustomInstruments = async (roundId) => { + return new Promise(async (resolve, reject) => { + let customInstruments = {} + try { + const instrumentSnapshot = await this.db + .collection("rounds") + .doc(roundId) + .collection('customInstruments') + .get(); + instrumentSnapshot.forEach(instDoc => { + let instrument = instDoc.data() + customInstruments = { ...customInstruments, ...instrument } + }) + + resolve(customInstruments) + } + catch (e) { + console.error(e) + reject(e) + } + }) + } + getUserBuses = async (roundId) => { return new Promise(async (resolve, reject) => { let userBuses = {} @@ -255,7 +303,7 @@ class Firebase { // console.log('createRound()', data); return new Promise(async (resolve, reject) => { let round = _.cloneDeep(data) - const layers = [...round.layers] + const layers = round && round.layers ? [...round.layers] : [] delete round.layers const userBuses = [] for (const [userId, userBus] of Object.entries(round.userBuses)) { @@ -331,7 +379,9 @@ class Firebase { try { await this.db.collection('samples') .doc(sample.id) - .set(sampleClone) + .set(sampleClone).catch(e => { + console.log('create sample error', e) + }) resolve() } catch (e) { console.error(e) @@ -397,6 +447,22 @@ class Firebase { }) } + updateCustomInstruments = async (roundId, userId, customInstruments) => { + let customInstrumentsClone = _.cloneDeep(customInstruments) + return new Promise(async (resolve, reject) => { + try { + await this.db.collection('rounds') + .doc(roundId) + .collection('customInstruments') + .doc(userId) + .set(customInstrumentsClone) + resolve() + } catch (e) { + console.error(e) + } + }) + } + saveUserPatterns = async (roundId, userId, userPatterns) => { console.log('saveUserPatterns()', roundId, userId, userPatterns); let userPatternsClone = _.cloneDeep(userPatterns) @@ -416,7 +482,6 @@ class Firebase { } updateRound = async (roundId, data) => { - // console.log('updateRound', roundId, data) try { await this.db.collection('rounds') .doc(roundId) @@ -428,7 +493,6 @@ class Firebase { } updateLayer = async (roundId, layerId, data) => { - // console.log('updateLayer', roundId, data) try { await this.db.collection('rounds') .doc(roundId) @@ -441,7 +505,6 @@ class Firebase { } updateUserBus = async (roundId, userId, userBus) => { - // console.log('firebase::updateUserBus()', roundId, userId, userBus); return new Promise(async (resolve, reject) => { try { await this.db.collection('rounds') diff --git a/src/redux/actionTypes.js b/src/redux/actionTypes.js index 722d010..5725c87 100644 --- a/src/redux/actionTypes.js +++ b/src/redux/actionTypes.js @@ -6,6 +6,8 @@ export const SET_USER_COLOR = "SET_USER_COLOR"; // Display export const SET_IS_SHOWING_SIGNIN_DIALOG = "SET_IS_SHOWING_SIGNIN_DIALOG"; +export const SET_IS_SHOWING_CREATE_ROUND_MODAL = "SET_IS_SHOWING_CREATE_ROUND_MODAL"; +export const SET_IS_SHOWING_CUSTOM_INSTRUMENT_DIALOG = "SET_IS_SHOWING_CUSTOM_INSTRUMENT_DIALOG"; export const SET_REDIRECT_AFTER_SIGN_IN = "SET_REDIRECT_AFTER_SIGN_IN"; export const SET_SIGNUP_DISPLAYNAME = "SET_SIGNUP_DISPLAYNAME"; export const SET_SELECTED_LAYER_ID = "SET_SELECTED_LAYER_ID"; @@ -28,6 +30,7 @@ export const SET_ROUNDS = "SET_ROUNDS"; // Round export const SET_ROUND = "SET_ROUND"; export const UPDATE_LAYERS = "UPDATE_LAYERS"; +export const UPDATE_CUSTOM_INSTRUMENTS = "UPDATE_CUSTOM_INSTRUMENTS"; export const TOGGLE_STEP = "TOGGLE_STEP"; export const ADD_LAYER = "ADD_LAYER"; export const SET_STEP_PROBABILITY = "SET_STEP_PROBABILITY"; diff --git a/src/redux/actions.js b/src/redux/actions.js index 5533538..2f5987d 100644 --- a/src/redux/actions.js +++ b/src/redux/actions.js @@ -1,5 +1,7 @@ import { SET_IS_SHOWING_SIGNIN_DIALOG, + SET_IS_SHOWING_CREATE_ROUND_MODAL, + SET_IS_SHOWING_CUSTOM_INSTRUMENT_DIALOG, SET_USER, SET_REDIRECT_AFTER_SIGN_IN, SET_ROUNDS, @@ -29,6 +31,7 @@ import { SET_IS_SHOWING_ORIENTATION_DIALOG, UPDATE_LAYER, UPDATE_LAYERS, + UPDATE_CUSTOM_INSTRUMENTS, SET_IS_RECORDING_SEQUENCE, SET_USER_PATTERN_SEQUENCE, SET_IS_PLAYING_SEQUENCE, @@ -67,6 +70,14 @@ export const setIsShowingSignInDialog = (value) => ({ type: SET_IS_SHOWING_SIGNIN_DIALOG, payload: { value } }) +export const setIsShowingCreateRoundDialog = (value) => ({ + type: SET_IS_SHOWING_CREATE_ROUND_MODAL, + payload: { value } +}) +export const setIsShowingCustomInstrumentDialog = (value) => ({ + type: SET_IS_SHOWING_CUSTOM_INSTRUMENT_DIALOG, + payload: { value } +}) export const setRedirectAfterSignIn = (value) => ({ type: SET_REDIRECT_AFTER_SIGN_IN, payload: { value } @@ -132,6 +143,10 @@ export const updateLayers = (layers) => ({ type: UPDATE_LAYERS, payload: { layers } }) +export const updateCustomInstruments = (customInstruments) => ({ + type: UPDATE_CUSTOM_INSTRUMENTS, + payload: { customInstruments } +}) export const setIsPlaying = (value) => ({ type: SET_IS_PLAYING, payload: { value } diff --git a/src/redux/reducers/display.js b/src/redux/reducers/display.js index 6df4cd9..e5edfc2 100644 --- a/src/redux/reducers/display.js +++ b/src/redux/reducers/display.js @@ -1,6 +1,8 @@ /* eslint-disable import/no-anonymous-default-export */ import { SET_IS_SHOWING_SIGNIN_DIALOG, + SET_IS_SHOWING_CREATE_ROUND_MODAL, + SET_IS_SHOWING_CUSTOM_INSTRUMENT_DIALOG, SET_REDIRECT_AFTER_SIGN_IN, SET_SIGNUP_DISPLAYNAME, SET_SELECTED_LAYER_ID, @@ -30,7 +32,9 @@ const initialState = { selectedRoundId: null, isShowingOrientationDialog: false, isRecordingSequence: false, - currentSequencePattern: null + currentSequencePattern: null, + isShowingCreateRoundDialog: false, + isShowingCustomInstrumentDialog: false }; export default function (state = initialState, action) { @@ -40,6 +44,16 @@ export default function (state = initialState, action) { isShowingSignInDialog: { $set: action.payload.value } }) } + case SET_IS_SHOWING_CREATE_ROUND_MODAL: { + return update(state, { + isShowingCreateRoundDialog: { $set: action.payload.value } + }) + } + case SET_IS_SHOWING_CUSTOM_INSTRUMENT_DIALOG: { + return update(state, { + isShowingCustomInstrumentDialog: { $set: action.payload.value } + }) + } case SET_REDIRECT_AFTER_SIGN_IN: { return update(state, { redirectAfterSignIn: { $set: action.payload.value } diff --git a/src/redux/reducers/round.js b/src/redux/reducers/round.js index ea354d2..e0f5228 100644 --- a/src/redux/reducers/round.js +++ b/src/redux/reducers/round.js @@ -2,6 +2,7 @@ import { SET_ROUND, UPDATE_LAYERS, + UPDATE_CUSTOM_INSTRUMENTS, TOGGLE_STEP, SET_STEP_VELOCITY, SET_STEP_PROBABILITY, @@ -41,8 +42,8 @@ import { import update from 'immutability-helper'; import _ from 'lodash' -const initialState = null; -const updateStepProperty = (state, name, value, layerId, stepId) => { +const initialState = null +const updateStepProperty = (state, name, value, layerId, stepId, lastUpdated) => { const layerIndex = _.findIndex(state.layers, { id: layerId }) const layer = _.find(state.layers, { id: layerId }) const stepIndex = _.findIndex(layer.steps, { id: stepId }) @@ -54,6 +55,9 @@ const updateStepProperty = (state, name, value, layerId, stepId) => { [stepIndex]: { [name]: { $set: value + }, + lastUpdated: { + $set: lastUpdated } } } @@ -73,7 +77,7 @@ export default function (state = initialState, action) { } case UPDATE_LAYERS: { - const { layers } = action.payload; + const { layers } = action.payload let layersUpdate = {} for (let i = 0; i < layers.length; i++) { layersUpdate[i] = { @@ -82,8 +86,18 @@ export default function (state = initialState, action) { } return update(state, { layers: layersUpdate - }); + }) } + + case UPDATE_CUSTOM_INSTRUMENTS: { + const { customInstruments } = action.payload + return update(state, { + customInstruments: { + $set: customInstruments + } + }) + } + case UPDATE_STEP: { const { step, layerId } = action.payload; const layerIndex = _.findIndex(state.layers, { id: layerId }) @@ -101,6 +115,7 @@ export default function (state = initialState, action) { } }); } + case ADD_STEP: { const { layerId, step } = action.payload; const layerIndex = _.findIndex(state.layers, { id: layerId }) @@ -114,6 +129,7 @@ export default function (state = initialState, action) { } }) } + case REMOVE_STEP: { const { layerId, stepId } = action.payload; const layerIndex = _.findIndex(state.layers, { id: layerId }) @@ -130,8 +146,8 @@ export default function (state = initialState, action) { }) } case TOGGLE_STEP: { - const { layerId, stepId, isOn } = action.payload; - return updateStepProperty(state, 'isOn', isOn, layerId, stepId); + const { layerId, stepId, isOn, lastUpdated } = action.payload; + return updateStepProperty(state, 'isOn', isOn, layerId, stepId, lastUpdated); } case SET_STEP_VELOCITY: { const { layerId, stepId, velocity } = action.payload; diff --git a/src/samples/Custom/index.js b/src/samples/Custom/index.js new file mode 100644 index 0000000..9febaf9 --- /dev/null +++ b/src/samples/Custom/index.js @@ -0,0 +1,30 @@ +const Samples = { + default: { + id: 'default', + label: 'default', + samples: [ + { + sample: null, + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 5490, + loop_start: 0, + loop_end: 5489, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + } + ] + } +} + +export default Samples \ No newline at end of file diff --git a/src/samples/HiHats/index.js b/src/samples/HiHats/index.js index 95497a1..b06a7b0 100644 --- a/src/samples/HiHats/index.js +++ b/src/samples/HiHats/index.js @@ -1,3 +1,740 @@ -const Samples = { "tight": { "id": "tight", "label": "Tight", "samples": [{ "sample": "samples/KSA_Hats_1b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 5490, "loop_start": 0, "loop_end": 5489, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_1a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 4211, "loop_start": 0, "loop_end": 4210, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "clean": { "id": "clean", "label": "Clean", "samples": [{ "sample": "samples/KSA_Hats_2b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 6063, "loop_start": 0, "loop_end": 6062, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_2a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 6415, "loop_start": 0, "loop_end": 6414, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "open": { "id": "open", "label": "Open", "samples": [{ "sample": "samples/KSA_Hats_3b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 32126, "loop_start": 0, "loop_end": 32125, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_3a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 96424, "loop_start": 0, "loop_end": 96423, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "pedal": { "id": "pedal", "label": "Pedal", "samples": [{ "sample": "samples/KSA_Hats_4b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 35015, "loop_start": 0, "loop_end": 35014, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_4a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 39050, "loop_start": 0, "loop_end": 39049, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "quick": { "id": "quick", "label": "Quick", "samples": [{ "sample": "samples/KSA_Hats_5b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 5115, "loop_start": 0, "loop_end": 5114, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_5a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 4211, "loop_start": 0, "loop_end": 4210, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "tick": { "id": "tick", "label": "Tick", "samples": [{ "sample": "samples/KSA_Hats_6b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 19690, "loop_start": 0, "loop_end": 19689, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_6a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 22182, "loop_start": 0, "loop_end": 22181, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "grunge": { "id": "grunge", "label": "Grunge", "samples": [{ "sample": "samples/KSA_Hats_7b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 21256, "loop_start": 0, "loop_end": 21255, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_7a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 16515, "loop_start": 0, "loop_end": 16514, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "earth": { "id": "earth", "label": "Earth", "samples": [{ "sample": "samples/KSA_Hats_8b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 9106, "loop_start": 0, "loop_end": 9105, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_8a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 14685, "loop_start": 0, "loop_end": 14684, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "dirt": { "id": "dirt", "label": "Dirt", "samples": [{ "sample": "samples/KSA_Hats_9b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 28356, "loop_start": 0, "loop_end": 28355, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_9a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 26967, "loop_start": 0, "loop_end": 26966, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "sprung": { "id": "sprung", "label": "Sprung", "samples": [{ "sample": "samples/KSA_Hats_10b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 17904, "loop_start": 0, "loop_end": 17903, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_10a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 19183, "loop_start": 0, "loop_end": 19182, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "split": { "id": "split", "label": "Split", "samples": [{ "sample": "samples/KSA_Hats_11b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 5512, "loop_start": 0, "loop_end": 5511, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_11a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 9371, "loop_start": 0, "loop_end": 9370, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "dash": { "id": "dash", "label": "Dash", "samples": [{ "sample": "samples/KSA_Hats_12b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 15853, "loop_start": 0, "loop_end": 15852, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_12a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 15853, "loop_start": 0, "loop_end": 15852, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "small": { "id": "small", "label": "Small", "samples": [{ "sample": "samples/KSA_Hats_13b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 12855, "loop_start": 0, "loop_end": 12854, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_13a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 5490, "loop_start": 0, "loop_end": 5489, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "cupped": { "id": "cupped", "label": "Cupped", "samples": [{ "sample": "samples/KSA_Hats_14b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 22027, "loop_start": 0, "loop_end": 22026, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_14a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 10694, "loop_start": 0, "loop_end": 10693, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "clear": { "id": "clear", "label": "Clear", "samples": [{ "sample": "samples/KSA_Hats_15b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 8709, "loop_start": 0, "loop_end": 8708, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_15a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 14905, "loop_start": 0, "loop_end": 14904, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] }, "live": { "id": "live", "label": "Live", "samples": [{ "sample": "samples/KSA_Hats_16b.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 0, "hivel": 64, "offset": 0, "end": 18168, "loop_start": 0, "loop_end": 18167, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }, { "sample": "samples/KSA_Hats_16a.wav", "volume": 3, "pitch_keycenter": 60, "lovel": 65, "hivel": 127, "offset": 0, "end": 9635, "loop_start": 0, "loop_end": 9634, "lokey": 0, "hikey": 127, "loop_mode": "no_loop", "ampeg_attack": "0.001", "amplfo_freq": "8.176", "fillfo_freq": "8.176", "pitchlfo_freq": "8.176", "fil_type": "lpf_2p", "cutoff": 19913 }] } } +const Samples = { + tight: { + id: 'tight', + label: 'Tight', + samples: [ + { + sample: 'samples/KSA_Hats_1b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 5490, + loop_start: 0, + loop_end: 5489, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_1a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 4211, + loop_start: 0, + loop_end: 4210, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + clean: { + id: 'clean', + label: 'Clean', + samples: [ + { + sample: 'samples/KSA_Hats_2b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 6063, + loop_start: 0, + loop_end: 6062, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_2a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 6415, + loop_start: 0, + loop_end: 6414, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + open: { + id: 'open', + label: 'Open', + samples: [ + { + sample: 'samples/KSA_Hats_3b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 32126, + loop_start: 0, + loop_end: 32125, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_3a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 96424, + loop_start: 0, + loop_end: 96423, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + pedal: { + id: 'pedal', + label: 'Pedal', + samples: [ + { + sample: 'samples/KSA_Hats_4b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 35015, + loop_start: 0, + loop_end: 35014, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_4a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 39050, + loop_start: 0, + loop_end: 39049, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + quick: { + id: 'quick', + label: 'Quick', + samples: [ + { + sample: 'samples/KSA_Hats_5b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 5115, + loop_start: 0, + loop_end: 5114, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_5a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 4211, + loop_start: 0, + loop_end: 4210, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + tick: { + id: 'tick', + label: 'Tick', + samples: [ + { + sample: 'samples/KSA_Hats_6b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 19690, + loop_start: 0, + loop_end: 19689, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_6a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 22182, + loop_start: 0, + loop_end: 22181, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + grunge: { + id: 'grunge', + label: 'Grunge', + samples: [ + { + sample: 'samples/KSA_Hats_7b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 21256, + loop_start: 0, + loop_end: 21255, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_7a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 16515, + loop_start: 0, + loop_end: 16514, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + earth: { + id: 'earth', + label: 'Earth', + samples: [ + { + sample: 'samples/KSA_Hats_8b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 9106, + loop_start: 0, + loop_end: 9105, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_8a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 14685, + loop_start: 0, + loop_end: 14684, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + dirt: { + id: 'dirt', + label: 'Dirt', + samples: [ + { + sample: 'samples/KSA_Hats_9b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 28356, + loop_start: 0, + loop_end: 28355, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_9a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 26967, + loop_start: 0, + loop_end: 26966, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + sprung: { + id: 'sprung', + label: 'Sprung', + samples: [ + { + sample: 'samples/KSA_Hats_10b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 17904, + loop_start: 0, + loop_end: 17903, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_10a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 19183, + loop_start: 0, + loop_end: 19182, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + split: { + id: 'split', + label: 'Split', + samples: [ + { + sample: 'samples/KSA_Hats_11b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 5512, + loop_start: 0, + loop_end: 5511, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_11a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 9371, + loop_start: 0, + loop_end: 9370, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + dash: { + id: 'dash', + label: 'Dash', + samples: [ + { + sample: 'samples/KSA_Hats_12b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 15853, + loop_start: 0, + loop_end: 15852, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_12a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 15853, + loop_start: 0, + loop_end: 15852, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + small: { + id: 'small', + label: 'Small', + samples: [ + { + sample: 'samples/KSA_Hats_13b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 12855, + loop_start: 0, + loop_end: 12854, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_13a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 5490, + loop_start: 0, + loop_end: 5489, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + cupped: { + id: 'cupped', + label: 'Cupped', + samples: [ + { + sample: 'samples/KSA_Hats_14b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 22027, + loop_start: 0, + loop_end: 22026, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_14a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 10694, + loop_start: 0, + loop_end: 10693, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + clear: { + id: 'clear', + label: 'Clear', + samples: [ + { + sample: 'samples/KSA_Hats_15b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 8709, + loop_start: 0, + loop_end: 8708, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_15a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 14905, + loop_start: 0, + loop_end: 14904, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, + live: { + id: 'live', + label: 'Live', + samples: [ + { + sample: 'samples/KSA_Hats_16b.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 0, + hivel: 64, + offset: 0, + end: 18168, + loop_start: 0, + loop_end: 18167, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + { + sample: 'samples/KSA_Hats_16a.wav', + volume: 3, + pitch_keycenter: 60, + lovel: 65, + hivel: 127, + offset: 0, + end: 9635, + loop_start: 0, + loop_end: 9634, + lokey: 0, + hikey: 127, + loop_mode: 'no_loop', + ampeg_attack: '0.001', + amplfo_freq: '8.176', + fillfo_freq: '8.176', + pitchlfo_freq: '8.176', + fil_type: 'lpf_2p', + cutoff: 19913, + }, + ], + }, +}; -export default Samples \ No newline at end of file +export default Samples; diff --git a/src/utils/constants.js b/src/utils/constants.js index 9fc1d73..7450718 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -1,23 +1,104 @@ export const HTML_UI_Params = { stepDiameter: 48, + sequenceButtonDiameter: 16, + sequenceButtonDots: 12, + dotDiameter: 3, + dotXOffset: 9, + dotYOffset: 10, addNewLayerButtonDiameter: 64, layerPadding: 16, + microLayerPadding: 4, + micro2LayerPadding: 2, initialLayerPadding: 384, + initialMicroLayerPadding: 20, + initialMicro2LayerPadding: 8, + microStepDiameter: 8, + micro2StepDiameter: 1.5, avatarDiameter: 128, avatarPadding: 128, + microLayerOffsetMultiplier: 6, + micro2LayerOffsetMultiplier: 2.6, avatarRoundPadding: 128, activityIndicatorDiameter: 48, activityAnimationTime: 700, stepAnimationUpdateTime: 200, stepStrokeWidth: 6, + microStepStrokeWidth: 0.4, layerStrokeMax: 48, + microLayerStrokeMax: 3, + micro2LayerStrokeMax: 2, layerStrokeOpacity: 0.3, stepModalDimensions: 200, stepModalThumbDiameter: 32, - otherUserLayerSizeDivisor: 3 + otherUserLayerSizeDivisor: 3, + initialLayerDiameter: 768, + patternsContainerDiameterOffset: 320, + patternsMainContainerDivisor: 2, + patternsLayerDiameterDivisor: 3.4, + tempoButtonWidth: 60, + tempoButtonHeight: 33, + tempoButtonRadius: 16, + sequenceButtonXOffset: 180, + sequenceButtonYOffset: 50, + stopSequenceIconXOffset: 202, + stopSequenceIconYOffset: 62, + stopSequenceButtonXOffset: 190, + stopSequenceButtonYOffset: 50, + tempoButtonXOffset: 196, + tempoButtonYOffset: 272, + tempoIconXOffset: 207, + tempoIconYOffset: 282, + tempoButtonTextXOffset: 226, + tempoButtonTextYOffset: 280, + sequenceTextXOffset: 213, + sequenceTextYOffset: 61.5, + stopSequenceTextXOffset: 222, + stopSequenceTextYOffset: 61.5, + anglePIDivisor: -1.335, + sequenceButtonWidth: 95, + sequenceButtonHeight: 36, + sequenceButtonRadius: 18, + stopSequenceButtonWidth: 70, + stopSequenceButtonHeight: 36, + sequenceBackgroundWidth: 8, + sequenceDiameterOffset: 15, + sequenceContainerDiameterOffset: 550, + sequencePatternXOffset: 114, + sequencePatternYOffset: 116, + sequenceLabelXOffset: 13, + sequenceLabelYOffset: 11, + sequenceBackgroundXOffset: 6, + sequenceBackgroundYOffset: 6, + sequenceBackgroundDiameterOffset: 12, + presetLabelXOffset: 15, + presetLabelYOffset: 10, + presetPatternOutlineXOffset: 12.5, + presetPatternOutlineYOffset: 12.5, + presetClickableButtonDiameterOffset: 20, + presetPatternOulineDiameterOffset: 25, + presetClickableButtonXOffset: 10, + presetClickableButtonYoffset: 10, + sequenceSwitchWidth: 70, + sequenceSwitchHeight: 38, + sequenceSwitchBorderRadius: 19, + sequenceSwitchXOffset: 190, + sequenceSwitchYOffset: 145, + sequenceSwitchDotOffset: 1, + sequenceSwitchDotsDiameterOffset: 4, + sequenceSwitchDotsXOffset: 46, + sequenceSwitchDotsYOffset: 13, + sequenceSwitchLabelSubContainerXOffset: 201.5, + sequenceSwitchLabelSubContainerYOffset: 156.5, + sequenceSwitchLabelXOffset: 205, + sequenceSwitchLabelYOffset: 158, + sequenceSwitchLabelContainerSize: 28, + sequenceSwitchLabelContainerOffXOffset: 195, + sequenceSwitchLabelContainerYOffset: 150, + sequenceSwitchLabelContainerONXOffset: 227, } export const Colors = ['#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5', '#2196f3', '#00bcd4', '#009688', '#4caf50', '#8bc34a', '#cddc39', '#ffeb3b', '#ffc107', '#ff9800', '#ff5722'] +export const PRESET_LETTERS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L'] export const KEY_MAPPINGS = { playToggle: ' ' @@ -45,7 +126,4 @@ export const Limits = { min: 1, max: 32 } -} - - - +} \ No newline at end of file diff --git a/src/utils/defaultData.js b/src/utils/defaultData.js index 2858850..346099c 100644 --- a/src/utils/defaultData.js +++ b/src/utils/defaultData.js @@ -1,7 +1,9 @@ import { Layer } from './constants'; import { uuid } from './index'; +import { randomInt } from './index'; +import Instruments from '../audio-engine/Instruments'; //import Track from '../audio-engine/Track' -import _ from 'lodash' +import _ from 'lodash'; export const refrashAllIdsInArray = (array) => { return array.map(item => ({ ...item, id: uuid() })) @@ -17,7 +19,13 @@ export const getDefaultStepData = () => { } }; -export const getDefaultLayerData = (userId, instrument) => { +export const getDefaultLayerData = async (userId, instrument) => { + const newInstruments = await Instruments.classes(); + const newInstrumentsKeyArray = Object.keys(newInstruments); + const upperLimit = newInstrumentsKeyArray.length - 1; + const instrumentNo = randomInt(0, upperLimit); + const randInstName = newInstrumentsKeyArray[instrumentNo]; + const randArticulation = await Instruments.getRandomArticulation(randInstName); const layer = { "id": uuid(), "createdBy": userId || null, @@ -32,8 +40,8 @@ export const getDefaultLayerData = (userId, instrument) => { "instrument": { "noteLength": "64n", "instrument": "Sampler", - "sampler": "HiHats", - "sample": "quick", + "sampler": randInstName, + "sample": randArticulation, ...instrument }, "steps": Array(Layer.DefaultStepsAmount).fill(null).map(() => { return getDefaultStepData() }), @@ -47,7 +55,67 @@ export const getDefaultLayerData = (userId, instrument) => { return layer; }; -export const getDefaultRoundData = (userId) => { +/** TODO: create new round using custom sounds */ + +export const generateLayers = async (samples, userId) => { + return new Promise(async resolve => { + const layers = [] + await samples.map(async (sample, i) => { + const articulation = await Instruments.create('custom', sample.id) + const layer = await getDefaultLayerData(userId, { + "instrument": "Sampler", + "sampler": 'custom', + "sample": articulation.name, + "sampleId": sample.id, + "displayName": sample.displayName + }) + layers.push(layer) + if (layers.length === samples.length) + resolve(layers) + }) + }) +} + +export const getDefaultRoundData = async (userId, samples) => { + if (samples && samples.length) { + await Instruments.init() + const layers = await generateLayers(samples, userId) + const round = { + "createdBy": userId || null, + "id": uuid(), + "dataVersion": 1.5, + "bpm": 120, + swing: 0, + "name": "Default Round", + "createdAt": Date.now(), + "currentUsers": [], + "customInstruments": {}, + "layers": layers, + userBuses: {}, + userPatterns: {} + } + round.userBuses[userId] = getDefaultUserBus(userId) + round.userPatterns[userId] = getDefaultUserPatterns(userId) + // increase each layer createdAt time by 1 ms so they're not equal + let i = 0 + for (let layer of round.layers) { + layer.name = "Layer " + (i + 1) + layer.createdAt += i++ + } + return round + } + const newInstruments = await Instruments.classes(); + const newInstrumentsKeyArray = Object.keys(newInstruments); + const upperLimit = newInstrumentsKeyArray.length - 1; + const instrumentNo = randomInt(0, upperLimit); + const instrumentNo1 = randomInt(0, upperLimit); + const instrumentNo2 = randomInt(0, upperLimit); + const randInstName = newInstrumentsKeyArray[instrumentNo]; + const randInstName1 = newInstrumentsKeyArray[instrumentNo1]; + const randInstName2 = newInstrumentsKeyArray[instrumentNo2]; + const randomArticulation = await Instruments.getRandomArticulation(randInstName); + const randomArticulation1 = await Instruments.getRandomArticulation(randInstName1); + const randomArticulation2 = await Instruments.getRandomArticulation(randInstName2); const round = { "createdBy": userId || null, "id": uuid(), @@ -58,20 +126,20 @@ export const getDefaultRoundData = (userId) => { "createdAt": Date.now(), "currentUsers": [], "layers": [ - getDefaultLayerData(userId, { + await getDefaultLayerData(userId, { "instrument": "Sampler", - "sampler": "HiHats", - "sample": "quick", + "sampler": randInstName, + "sample": randomArticulation, }), - getDefaultLayerData(userId, { + await getDefaultLayerData(userId, { "instrument": "Sampler", - "sampler": "Snares", - "sample": "skintight", + "sampler": randInstName1, + "sample": randomArticulation1, }), - getDefaultLayerData(userId, { + await getDefaultLayerData(userId, { "instrument": "Sampler", - "sampler": "Kicks", - "sample": "classic" + "sampler": randInstName2, + "sample": randomArticulation2, }) ], userBuses: {}, @@ -99,39 +167,46 @@ export const getDefaultUserBusFx = () => { return [ { "id": uuid(), - name: 'autowah', + name: 'pingpong', order: 0, isOn: true, isOverride: false }, { "id": uuid(), - name: 'lowpass', + name: 'autowah', order: 1, isOn: true, isOverride: false }, { "id": uuid(), - name: 'highpass', + name: 'delay', order: 2, isOn: true, isOverride: false }, { "id": uuid(), - name: 'delay', + name: 'distortion', order: 3, isOn: true, isOverride: false }, { "id": uuid(), - name: 'distortion', + name: 'lowpass', order: 4, isOn: true, isOverride: false - } + }, + { + "id": uuid(), + name: 'highpass', + order: 5, + isOn: true, + isOverride: false + }, ] } diff --git a/src/utils/index.js b/src/utils/index.js index 5758615..e8c996d 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,10 +1,19 @@ /* eslint-disable eqeqeq */ import { getDefaultRoundData, getDefaultStepData } from './defaultData' -import { Limits, Colors } from './constants' -import _ from 'lodash' +import { Colors } from './constants' -export const createRound = (userId) => { - return getDefaultRoundData(userId) +export const createRound = async (userId, samples) => { + return await getDefaultRoundData(userId, samples) +} + +export const randomInt = (min, max) => { + return Math.floor(Math.random() * (max - min)) + min; +} + +export const arraymove = async (arr, fromIndex, toIndex) => { + var element = arr[fromIndex]; + arr.splice(fromIndex, 1); + arr.splice(toIndex, 0, element); } export const uuid = () => { diff --git a/yarn.lock b/yarn.lock index e3fd8be..05e3c40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1147,6 +1147,39 @@ resolved "https://registry.yarnpkg.com/@csstools/normalize.css/-/normalize.css-10.1.0.tgz#f0950bba18819512d42f7197e56c518aa491cf18" integrity sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg== +"@cypress/request@^2.88.6": + version "2.88.6" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.6.tgz#a970dd675befc6bdf8a8921576c01f51cc5798e9" + integrity sha512-z0UxBE/+qaESAHY9p9sM2h8Y4XqtsbDCt0/DPOrqA/RZgKi4PkxdpXyK4wCCnSk1xHqWHZZAE+gV6aDAR6+caQ== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^8.3.2" + +"@cypress/xvfb@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" + integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== + dependencies: + debug "^3.1.0" + lodash.once "^4.1.1" + "@emotion/hash@^0.8.0": version "0.8.0" resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.8.0.tgz#bbbff68978fefdbe68ccb533bc8cbe1d1afb5413" @@ -1854,9 +1887,9 @@ integrity sha512-yZ4jfP/SeLHEnCi9PIrzienKCrA4vW9+jm5uUV3N5DG2e9zgXLY5FgywK2u8/gMFIeKO0HuqTLFFfWJj+MfMLA== "@svgdotjs/svg.panzoom.js@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@svgdotjs/svg.panzoom.js/-/svg.panzoom.js-2.1.1.tgz#97ac97010d7814fdef84c8bdbb91b56b4e3edaf6" - integrity sha512-fsGP+i64jcpaG1UXOmNDUWq2ZLpiMf+EwfHZn5NNnYp5K7J2gk96NcPhzMtgmCUZtu61Ro8Z5D2P3RdzBzpc3g== + version "2.1.2" + resolved "https://registry.yarnpkg.com/@svgdotjs/svg.panzoom.js/-/svg.panzoom.js-2.1.2.tgz#50e66a861f7c4f9e3992707f8e62e6e8da5c5223" + integrity sha512-0Nzo2TRlTebW3pzfAPtHx8Ye7Y3kuMEkK7hwVJi0SgQUB/vstjg7fvCJxB++EqsuDEetP0/SC+4CpLMVm6Lh2g== dependencies: "@svgdotjs/svg.js" "^3.0.16" @@ -2325,6 +2358,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.41.tgz#045a4981318d31a581650ce70f340a32c3461198" integrity sha512-qLT9IvHiXJfdrje9VmsLzun7cQ65obsBTmtU3EOnCSLFOoSHx1hpiRHoBnpdbyFqnzqdUUIv81JcEJQCB8un9g== +"@types/node@^14.14.31": + version "14.17.12" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.17.12.tgz#7a31f720b85a617e54e42d24c4ace136601656c7" + integrity sha512-vhUqgjJR1qxwTWV5Ps5txuy2XMdf7Fw+OrdChRboy8BmWUPkckOhphaohzFG6b8DW7CrxaBMdrdJ47SYFq1okw== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -2372,6 +2410,16 @@ dependencies: "@types/node" "*" +"@types/sinonjs__fake-timers@^6.0.2": + version "6.0.3" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.3.tgz#79df6f358ae8f79e628fe35a63608a0ea8e7cf08" + integrity sha512-E1dU4fzC9wN2QK2Cr1MLCfyHM8BoNnRFvuf45LYMPNDA+WqbNzC45S4UzPxvp1fFJ1rvSGU0bPvdd35VLmXG8g== + +"@types/sizzle@^2.3.2": + version "2.3.3" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef" + integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ== + "@types/source-list-map@*": version "0.1.2" resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" @@ -2434,6 +2482,13 @@ dependencies: "@types/yargs-parser" "*" +"@types/yauzl@^2.9.1": + version "2.9.2" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.9.2.tgz#c48e5d56aff1444409e39fa164b0b4d4552a7b7a" + integrity sha512-8uALY5LTvSuHgloDVUvWP3pIauILm+8/0pDMokuDYIoNsOkSwd5AiHBTSEJjKTDcZr5z8UpgOWZkxBF4iJftoA== + dependencies: + "@types/node" "*" + "@typescript-eslint/eslint-plugin@^4.5.0": version "4.14.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.14.2.tgz#47a15803cfab89580b96933d348c2721f3d2f6fe" @@ -2825,6 +2880,13 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.1: dependencies: type-fest "^0.11.0" +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + ansi-html@0.0.7, ansi-html@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -2880,6 +2942,11 @@ aproba@^1.1.1: resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== +arch@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" + integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -3054,6 +3121,11 @@ async@^2.6.2: dependencies: lodash "^4.17.14" +async@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.1.tgz#d3274ec66d107a47476a4c49136aacdb00665fc8" + integrity sha512-XdD5lRO/87udXCMC9meWdYiR+Nq6ZjUfXidViUZGu2F1MO4T3XwZ1et0hb2++BgLfhyJwy44BGB/yx80ABx8hg== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -3351,7 +3423,12 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" -bluebird@^3.5.5: +blob-util@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" + integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== + +bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -3542,6 +3619,11 @@ buffer-alloc@^1.2.0: buffer-alloc-unsafe "^1.1.0" buffer-fill "^1.0.0" +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" @@ -3663,6 +3745,11 @@ cache-base@^1.0.1: union-value "^1.0.0" unset-value "^1.0.0" +cachedir@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.3.0.tgz#0c75892a052198f0b21c7c1804d8331edfcae0e8" + integrity sha512-A+Fezp4zxnit6FanDmv9EqXNAi3vt9DWp51/71UEhXukb7QUuvtv9344h91dyAxuTLoSYJFU299qzR3tzwPAhw== + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -3775,6 +3862,11 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +check-more-types@^2.24.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" + integrity sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA= + check-types@^11.1.1: version "11.1.2" resolved "https://registry.yarnpkg.com/check-types/-/check-types-11.1.2.tgz#86a7c12bf5539f6324eb0e70ca8896c0e38f3e2f" @@ -3836,6 +3928,11 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== +ci-info@^3.1.1: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.2.0.tgz#2876cb948a498797b5236f0095bc057d0dca38b6" + integrity sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A== + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" @@ -3871,6 +3968,31 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-table3@~0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.0.tgz#b7b1bc65ca8e7b5cef9124e13dc2b21e2ce4faee" + integrity sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ== + dependencies: + object-assign "^4.1.0" + string-width "^4.2.0" + optionalDependencies: + colors "^1.1.2" + +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -3975,6 +4097,16 @@ colorette@^1.2.1: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.1.tgz#4d0b921325c14faf92633086a536db6e89564b1b" integrity sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw== +colorette@^1.2.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.3.0.tgz#ff45d2f0edb244069d3b772adeb04fed38d0a0af" + integrity sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w== + +colors@^1.1.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + combined-stream@^1.0.6, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -3992,6 +4124,11 @@ commander@^4.1.1: resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + common-tags@^1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937" @@ -4520,6 +4657,53 @@ cyclist@^1.0.1: resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9" integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk= +cypress@^8.3.1: + version "8.3.1" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-8.3.1.tgz#c6760dbb907df2570b0e1ac235fa31c30f9260a6" + integrity sha512-1v6pfx+/5cXhaT5T6QKOvnkawmEHWHLiVzm3MYMoQN1fkX2Ma1C32STd3jBStE9qT5qPSTILjGzypVRxCBi40g== + dependencies: + "@cypress/request" "^2.88.6" + "@cypress/xvfb" "^1.2.4" + "@types/node" "^14.14.31" + "@types/sinonjs__fake-timers" "^6.0.2" + "@types/sizzle" "^2.3.2" + arch "^2.2.0" + blob-util "^2.0.2" + bluebird "^3.7.2" + cachedir "^2.3.0" + chalk "^4.1.0" + check-more-types "^2.24.0" + cli-cursor "^3.1.0" + cli-table3 "~0.6.0" + commander "^5.1.0" + common-tags "^1.8.0" + dayjs "^1.10.4" + debug "^4.3.2" + enquirer "^2.3.6" + eventemitter2 "^6.4.3" + execa "4.1.0" + executable "^4.1.1" + extract-zip "2.0.1" + figures "^3.2.0" + fs-extra "^9.1.0" + getos "^3.2.1" + is-ci "^3.0.0" + is-installed-globally "~0.4.0" + lazy-ass "^1.6.0" + listr2 "^3.8.3" + lodash "^4.17.21" + log-symbols "^4.0.0" + minimist "^1.2.5" + ospath "^1.2.2" + pretty-bytes "^5.6.0" + ramda "~0.27.1" + request-progress "^3.0.0" + supports-color "^8.1.1" + tmp "~0.2.1" + untildify "^4.0.0" + url "^0.11.0" + yauzl "^2.10.0" + d@1, d@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" @@ -4549,6 +4733,11 @@ data-urls@^2.0.0: whatwg-mimetype "^2.3.0" whatwg-url "^8.0.0" +dayjs@^1.10.4: + version "1.10.6" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.6.tgz#288b2aa82f2d8418a6c9d4df5898c0737ad02a63" + integrity sha512-AztC/IOW4L1Q41A86phW5Thhcrco3xuAA+YX/BLpLWWjRcTj5TOt/QImBLmCKlrF7u7k47arTnOyL6GnbG8Hvw== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -4563,13 +4752,20 @@ debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: dependencies: ms "2.1.2" -debug@^3.1.1, debug@^3.2.5: +debug@^3.1.0, debug@^3.1.1, debug@^3.2.5: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" +debug@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" + integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + dependencies: + ms "2.1.2" + decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -4985,7 +5181,7 @@ enhanced-resolve@^4.3.0: memory-fs "^0.5.0" tapable "^1.0.0" -enquirer@^2.3.5: +enquirer@^2.3.5, enquirer@^2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -5150,6 +5346,13 @@ eslint-module-utils@^2.6.0: debug "^2.6.9" pkg-dir "^2.0.0" +eslint-plugin-cypress@^2.12.1: + version "2.12.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.12.1.tgz#9aeee700708ca8c058e00cdafe215199918c2632" + integrity sha512-c2W/uPADl5kospNDihgiLc7n87t5XhUbFDoTl6CfVkmG+kDAb5Ux10V9PoLPu9N+r7znpc+iQlcmAqT1A/89HA== + dependencies: + globals "^11.12.0" + eslint-plugin-flowtype@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.2.0.tgz#a4bef5dc18f9b2bdb41569a4ab05d73805a3d261" @@ -5385,6 +5588,11 @@ event-target-shim@^5.0.0: resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== +eventemitter2@^6.4.3: + version "6.4.4" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.4.tgz#aa96e8275c4dbeb017a5d0e03780c65612a1202b" + integrity sha512-HLU3NDY6wARrLCEwyGKRBvuWYyvW6mHYv72SJJAH3iJN3a6eVUvkjFkcxah1bcTgGVBBrFdIopBJPhCQFMLyXw== + eventemitter3@^4.0.0: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -5415,6 +5623,21 @@ exec-sh@^0.3.2: resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== +execa@4.1.0, execa@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + execa@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" @@ -5428,20 +5651,12 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" - integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== +executable@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" + integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== dependencies: - cross-spawn "^7.0.0" - get-stream "^5.0.0" - human-signals "^1.1.1" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.0" - onetime "^5.1.0" - signal-exit "^3.0.2" - strip-final-newline "^2.0.0" + pify "^2.2.0" exit@^0.1.2: version "0.1.2" @@ -5550,6 +5765,17 @@ extglob@^2.0.4: snapdragon "^0.8.1" to-regex "^3.0.1" +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" @@ -5620,11 +5846,25 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + dependencies: + pend "~1.2.0" + figgy-pudding@^3.5.1: version "3.5.2" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e" integrity sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw== +figures@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + file-entry-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.0.tgz#7921a89c391c6d93efec2169ac6bf300c527ea0a" @@ -5853,7 +6093,7 @@ fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.1: +fs-extra@^9.0.1, fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -5963,7 +6203,7 @@ get-stream@^4.0.0: dependencies: pump "^3.0.0" -get-stream@^5.0.0: +get-stream@^5.0.0, get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== @@ -5975,6 +6215,13 @@ get-value@^2.0.3, get-value@^2.0.6: resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= +getos@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" + integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== + dependencies: + async "^3.2.0" + getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -6009,6 +6256,13 @@ glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +global-dirs@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.0.tgz#70a76fe84ea315ab37b1f5576cbde7d48ef72686" + integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA== + dependencies: + ini "2.0.0" + global-modules@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780" @@ -6025,7 +6279,7 @@ global-prefix@^3.0.0: kind-of "^6.0.2" which "^1.3.1" -globals@^11.1.0: +globals@^11.1.0, globals@^11.12.0: version "11.12.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== @@ -6432,6 +6686,11 @@ human-signals@^1.1.1: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +husky@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.2.tgz#21900da0f30199acca43a46c043c4ad84ae88dff" + integrity sha512-8yKEWNX4z2YsofXAMT7KvA1g8p+GxtB1ffV8XtpAEGuXNAbCV5wdNKH+qTpw8SM9fh4aMPDR+yQuKfgnreyZlg== + hyphenate-style-name@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" @@ -6589,6 +6848,11 @@ inherits@2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + ini@^1.3.5: version "1.3.8" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" @@ -6705,6 +6969,13 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" +is-ci@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.0.tgz#c7e7be3c9d8eef7d0fa144390bd1e4b88dc4c994" + integrity sha512-kDXyttuLeslKAHYL/K28F2YkM3x5jvFPEw3yXbRptXydjD9rpLEz+C5K5iutY9ZiUu6AP41JdvRQwF4Iqs4ZCQ== + dependencies: + ci-info "^3.1.1" + is-color-stop@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" @@ -6822,6 +7093,14 @@ is-in-browser@^1.0.2, is-in-browser@^1.1.3: resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= +is-installed-globally@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + is-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" @@ -6873,6 +7152,11 @@ is-path-inside@^2.1.0: dependencies: path-is-inside "^1.0.2" +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-obj@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -6947,6 +7231,11 @@ is-typedarray@^1.0.0, is-typedarray@~1.0.0: resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -7774,6 +8063,11 @@ last-call-webpack-plugin@^3.0.0: lodash "^4.17.5" webpack-sources "^1.1.0" +lazy-ass@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" + integrity sha1-eZllXoZGwX8In90YfRUNMyTVRRM= + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -7800,6 +8094,19 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= +listr2@^3.8.3: + version "3.11.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.11.0.tgz#9771b02407875aa78e73d6e0ff6541bbec0aaee9" + integrity sha512-XLJVe2JgXCyQTa3FbSv11lkKExYmEyA4jltVo8z4FX10Vt1Yj8IMekBfwim0BSOM9uj1QMTJvDQQpHyuPbB/dQ== + dependencies: + cli-truncate "^2.1.0" + colorette "^1.2.2" + log-update "^4.0.0" + p-map "^4.0.0" + rxjs "^6.6.7" + through "^2.3.8" + wrap-ansi "^7.0.0" + load-json-file@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" @@ -7885,6 +8192,11 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= +lodash.once@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" @@ -7915,6 +8227,29 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + loglevel@^1.6.8: version "1.7.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.7.1.tgz#005fde2f5e6e47068f935ff28573e125ef72f197" @@ -8697,6 +9032,11 @@ os-browserify@^0.3.0: resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc= +ospath@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" + integrity sha1-EnZjl3Sj+O8lcvf+QoDg6kVQwHs= + p-each-series@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-2.2.0.tgz#105ab0357ce72b202a8a8b94933672657b5e2a9a" @@ -8946,6 +9286,11 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" @@ -8956,7 +9301,7 @@ picomatch@^2.0.4, picomatch@^2.0.5, picomatch@^2.2.1, picomatch@^2.2.2: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== -pify@^2.0.0: +pify@^2.0.0, pify@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= @@ -9735,6 +10080,11 @@ pretty-bytes@^5.3.0: resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.5.0.tgz#0cecda50a74a941589498011cf23275aa82b339e" integrity sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA== +pretty-bytes@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + pretty-error@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/pretty-error/-/pretty-error-2.1.2.tgz#be89f82d81b1c86ec8fdfbc385045882727f93b6" @@ -9949,6 +10299,11 @@ raf@^3.4.1: dependencies: performance-now "^2.1.0" +ramda@~0.27.1: + version "0.27.1" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.1.tgz#66fc2df3ef873874ffc2da6aa8984658abacf5c9" + integrity sha512-PgIdVpn5y5Yns8vqb8FzBUEYn98V3xcPgawAkkgj0YJ0qDsnHCiNmZYfOGMgOvoB0eWFLpYbhxUR3mxfDIMvpw== + randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -10426,6 +10781,13 @@ repeat-string@^1.6.1: resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= +request-progress@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" + integrity sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4= + dependencies: + throttleit "^1.0.0" + request-promise-core@1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.4.tgz#3eedd4223208d419867b78ce815167d10593a22f" @@ -10559,6 +10921,14 @@ resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.1 is-core-module "^2.1.0" path-parse "^1.0.6" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -10671,6 +11041,13 @@ run-queue@^1.0.0, run-queue@^1.0.3: dependencies: aproba "^1.1.1" +rxjs@^6.6.7: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -10981,6 +11358,15 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + slice-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" @@ -11458,6 +11844,13 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + supports-hyperlinks@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz#f663df252af5f37c5d49bbd7eeefa9e0b9e59e47" @@ -11616,6 +12009,11 @@ throat@^5.0.0: resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= + through2@^2.0.0: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" @@ -11624,6 +12022,11 @@ through2@^2.0.0: readable-stream "~2.3.6" xtend "~4.0.1" +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + thunky@^1.0.2: version "1.1.0" resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" @@ -11656,6 +12059,13 @@ tinycolor2@^1.4.1: resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803" integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA== +tmp@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" @@ -11818,6 +12228,11 @@ type-fest@^0.11.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + type-fest@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" @@ -11955,6 +12370,11 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + upath@^1.1.1, upath@^1.1.2, upath@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" @@ -12054,7 +12474,7 @@ uuid@^3.3.2, uuid@^3.4.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.0: +uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== @@ -12537,6 +12957,15 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -12653,6 +13082,14 @@ yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"