From d11c71b5b7ed26bb1279e4cd45180db600cdb8d0 Mon Sep 17 00:00:00 2001 From: Egor Date: Tue, 14 Feb 2017 23:55:12 -0800 Subject: [PATCH] chrome skeleton --- chrome/.bowerrc | 4 + chrome/.editorconfig | 20 + chrome/.ember-cli | 10 + chrome/.gitignore | 18 + chrome/.jshintrc | 37 + chrome/.travis.yml | 24 + chrome/.watchmanconfig | 3 + chrome/README.md | 44 ++ chrome/app/app.js | 18 + chrome/app/index.html | 30 + chrome/app/pods/application/controller.js | 54 ++ chrome/app/pods/application/template.hbs | 3 + .../components/bridge-finder/component.js | 152 ++++ .../components/bridge-finder/template.hbs | 49 ++ .../pods/components/hue-controls/component.js | 245 ++++++ .../pods/components/hue-controls/template.hbs | 49 ++ .../pods/components/huegasm-app/component.js | 37 + .../pods/components/huegasm-app/template.hbs | 6 + .../components/huegasm-footer/component.js | 21 + .../components/huegasm-footer/template.hbs | 13 + .../pods/components/light-group/component.js | 161 ++++ .../pods/components/light-group/template.hbs | 11 + .../lights-tab/color-picker/component.js | 58 ++ .../lights-tab/color-picker/template.hbs | 1 + .../pods/components/lights-tab/component.js | 348 +++++++++ .../pods/components/lights-tab/template.hbs | 43 + .../add-soundcloud-sound-modal/component.js | 46 ++ .../add-soundcloud-sound-modal/template.hbs | 14 + .../pods/components/music-tab/component.js | 737 ++++++++++++++++++ .../components/music-tab/mixins/helpers.js | 412 ++++++++++ .../components/music-tab/mixins/visualizer.js | 94 +++ .../pods/components/music-tab/template.hbs | 194 +++++ chrome/app/resolver.js | 3 + chrome/app/router.js | 12 + chrome/app/styles/app.scss | 118 +++ chrome/app/styles/bootstrap.scss | 56 ++ chrome/app/styles/bridge-finder.scss | 73 ++ chrome/app/styles/common.scss | 15 + chrome/app/styles/dimmer.scss | 76 ++ chrome/app/styles/fancy-speaker.scss | 101 +++ chrome/app/styles/hue-controls.scss | 112 +++ chrome/app/styles/huegasm-variables.scss | 8 + chrome/app/styles/introjs.scss | 18 + chrome/app/styles/light-group.scss | 71 ++ chrome/app/styles/music-tab.scss | 496 ++++++++++++ chrome/app/styles/noui-slider.scss | 56 ++ chrome/app/styles/paper.scss | 72 ++ chrome/bower.json | 14 + chrome/config/environment.js | 47 ++ chrome/ember-cli-build.js | 29 + chrome/package.json | 46 ++ chrome/public/128x128.png | Bin 0 -> 9408 bytes chrome/public/16x16.png | Bin 0 -> 1025 bytes chrome/public/32x32.png | Bin 0 -> 1732 bytes chrome/public/48x48.png | Bin 0 -> 3199 bytes chrome/public/assets/images/colormap.png | Bin 0 -> 6305 bytes .../assets/images/google-play-badge.png | Bin 0 -> 4219 bytes chrome/public/assets/images/huegasm.png | Bin 0 -> 3876 bytes chrome/public/assets/images/lights/a19.svg | 32 + chrome/public/assets/images/lights/a19w.svg | 32 + chrome/public/assets/images/lights/br30.svg | 48 ++ chrome/public/assets/images/lights/br30w.svg | 48 ++ chrome/public/assets/images/lights/gu10.svg | 18 + chrome/public/assets/images/lights/gu10w.svg | 18 + chrome/public/assets/images/lights/huego.svg | 23 + chrome/public/assets/images/lights/huegow.svg | 23 + .../public/assets/images/lights/lc_aura.svg | 32 + .../public/assets/images/lights/lc_auraw.svg | 32 + .../public/assets/images/lights/lc_bloom.svg | 30 + .../public/assets/images/lights/lc_bloomw.svg | 30 + .../public/assets/images/lights/lc_iris.svg | 29 + .../public/assets/images/lights/lc_irisw.svg | 29 + .../assets/images/lights/lightstrip.svg | 45 ++ .../assets/images/lights/lightstripw.svg | 45 ++ .../assets/images/lights/storylight.svg | 42 + .../assets/images/lights/storylightw.svg | 42 + chrome/public/assets/images/logo.png | Bin 0 -> 28287 bytes .../public/assets/images/missingArtwork.png | Bin 0 -> 1867 bytes .../assets/images/pressButtonBridge.png | Bin 0 -> 7050 bytes chrome/public/assets/images/sc-white-sm.png | Bin 0 -> 292 bytes chrome/public/assets/images/sc-white.png | Bin 0 -> 3452 bytes chrome/public/assets/images/soundcloudUrl.png | Bin 0 -> 56037 bytes chrome/public/manifest.json | 28 + chrome/vendor/dancer.js | 709 +++++++++++++++++ mobile/package.json | 1 - web/package.json | 3 +- 86 files changed, 5715 insertions(+), 3 deletions(-) create mode 100644 chrome/.bowerrc create mode 100644 chrome/.editorconfig create mode 100644 chrome/.ember-cli create mode 100644 chrome/.gitignore create mode 100644 chrome/.jshintrc create mode 100644 chrome/.travis.yml create mode 100644 chrome/.watchmanconfig create mode 100644 chrome/README.md create mode 100644 chrome/app/app.js create mode 100644 chrome/app/index.html create mode 100644 chrome/app/pods/application/controller.js create mode 100644 chrome/app/pods/application/template.hbs create mode 100644 chrome/app/pods/components/bridge-finder/component.js create mode 100644 chrome/app/pods/components/bridge-finder/template.hbs create mode 100644 chrome/app/pods/components/hue-controls/component.js create mode 100644 chrome/app/pods/components/hue-controls/template.hbs create mode 100644 chrome/app/pods/components/huegasm-app/component.js create mode 100644 chrome/app/pods/components/huegasm-app/template.hbs create mode 100644 chrome/app/pods/components/huegasm-footer/component.js create mode 100644 chrome/app/pods/components/huegasm-footer/template.hbs create mode 100644 chrome/app/pods/components/light-group/component.js create mode 100644 chrome/app/pods/components/light-group/template.hbs create mode 100644 chrome/app/pods/components/lights-tab/color-picker/component.js create mode 100644 chrome/app/pods/components/lights-tab/color-picker/template.hbs create mode 100644 chrome/app/pods/components/lights-tab/component.js create mode 100644 chrome/app/pods/components/lights-tab/template.hbs create mode 100644 chrome/app/pods/components/music-tab/add-soundcloud-sound-modal/component.js create mode 100644 chrome/app/pods/components/music-tab/add-soundcloud-sound-modal/template.hbs create mode 100644 chrome/app/pods/components/music-tab/component.js create mode 100644 chrome/app/pods/components/music-tab/mixins/helpers.js create mode 100644 chrome/app/pods/components/music-tab/mixins/visualizer.js create mode 100644 chrome/app/pods/components/music-tab/template.hbs create mode 100644 chrome/app/resolver.js create mode 100644 chrome/app/router.js create mode 100644 chrome/app/styles/app.scss create mode 100644 chrome/app/styles/bootstrap.scss create mode 100644 chrome/app/styles/bridge-finder.scss create mode 100644 chrome/app/styles/common.scss create mode 100644 chrome/app/styles/dimmer.scss create mode 100644 chrome/app/styles/fancy-speaker.scss create mode 100644 chrome/app/styles/hue-controls.scss create mode 100644 chrome/app/styles/huegasm-variables.scss create mode 100644 chrome/app/styles/introjs.scss create mode 100644 chrome/app/styles/light-group.scss create mode 100644 chrome/app/styles/music-tab.scss create mode 100644 chrome/app/styles/noui-slider.scss create mode 100644 chrome/app/styles/paper.scss create mode 100644 chrome/bower.json create mode 100644 chrome/config/environment.js create mode 100644 chrome/ember-cli-build.js create mode 100644 chrome/package.json create mode 100644 chrome/public/128x128.png create mode 100644 chrome/public/16x16.png create mode 100644 chrome/public/32x32.png create mode 100644 chrome/public/48x48.png create mode 100644 chrome/public/assets/images/colormap.png create mode 100644 chrome/public/assets/images/google-play-badge.png create mode 100644 chrome/public/assets/images/huegasm.png create mode 100644 chrome/public/assets/images/lights/a19.svg create mode 100644 chrome/public/assets/images/lights/a19w.svg create mode 100644 chrome/public/assets/images/lights/br30.svg create mode 100644 chrome/public/assets/images/lights/br30w.svg create mode 100644 chrome/public/assets/images/lights/gu10.svg create mode 100644 chrome/public/assets/images/lights/gu10w.svg create mode 100644 chrome/public/assets/images/lights/huego.svg create mode 100644 chrome/public/assets/images/lights/huegow.svg create mode 100644 chrome/public/assets/images/lights/lc_aura.svg create mode 100644 chrome/public/assets/images/lights/lc_auraw.svg create mode 100644 chrome/public/assets/images/lights/lc_bloom.svg create mode 100644 chrome/public/assets/images/lights/lc_bloomw.svg create mode 100644 chrome/public/assets/images/lights/lc_iris.svg create mode 100644 chrome/public/assets/images/lights/lc_irisw.svg create mode 100644 chrome/public/assets/images/lights/lightstrip.svg create mode 100644 chrome/public/assets/images/lights/lightstripw.svg create mode 100644 chrome/public/assets/images/lights/storylight.svg create mode 100644 chrome/public/assets/images/lights/storylightw.svg create mode 100644 chrome/public/assets/images/logo.png create mode 100644 chrome/public/assets/images/missingArtwork.png create mode 100644 chrome/public/assets/images/pressButtonBridge.png create mode 100644 chrome/public/assets/images/sc-white-sm.png create mode 100644 chrome/public/assets/images/sc-white.png create mode 100644 chrome/public/assets/images/soundcloudUrl.png create mode 100644 chrome/public/manifest.json create mode 100644 chrome/vendor/dancer.js diff --git a/chrome/.bowerrc b/chrome/.bowerrc new file mode 100644 index 0000000..959e169 --- /dev/null +++ b/chrome/.bowerrc @@ -0,0 +1,4 @@ +{ + "directory": "bower_components", + "analytics": false +} diff --git a/chrome/.editorconfig b/chrome/.editorconfig new file mode 100644 index 0000000..219985c --- /dev/null +++ b/chrome/.editorconfig @@ -0,0 +1,20 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 2 + +[*.hbs] +insert_final_newline = false + +[*.{diff,md}] +trim_trailing_whitespace = false diff --git a/chrome/.ember-cli b/chrome/.ember-cli new file mode 100644 index 0000000..b4934f3 --- /dev/null +++ b/chrome/.ember-cli @@ -0,0 +1,10 @@ +{ + /** + Ember CLI sends analytics information by default. The data is completely + anonymous, but there are times when you might want to disable this behavior. + + Setting `disableAnalytics` to true will prevent any data from being sent. + */ + "disableAnalytics": true, + "usePods": true +} diff --git a/chrome/.gitignore b/chrome/.gitignore new file mode 100644 index 0000000..8756776 --- /dev/null +++ b/chrome/.gitignore @@ -0,0 +1,18 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp + +# dependencies +/node_modules +/bower_components + +# misc +/.sass-cache +/connect.lock +/coverage/* +/libpeerconnection.log +npm-debug.log +testem.log +/.idea/ diff --git a/chrome/.jshintrc b/chrome/.jshintrc new file mode 100644 index 0000000..42624ff --- /dev/null +++ b/chrome/.jshintrc @@ -0,0 +1,37 @@ +{ + "predef": [ + "document", + "window", + "-Promise", + "Dancer", + "ID3", + "FileAPIReader", + "SC", + "introJs" + ], + "browser": true, + "boss": true, + "curly": true, + "debug": false, + "devel": true, + "eqeqeq": true, + "evil": true, + "forin": false, + "immed": false, + "laxbreak": false, + "newcap": true, + "noarg": true, + "noempty": false, + "nonew": false, + "nomen": false, + "onevar": false, + "plusplus": false, + "regexp": false, + "undef": true, + "sub": true, + "strict": false, + "white": false, + "eqnull": true, + "esversion": 6, + "unused": true +} diff --git a/chrome/.travis.yml b/chrome/.travis.yml new file mode 100644 index 0000000..a75f20e --- /dev/null +++ b/chrome/.travis.yml @@ -0,0 +1,24 @@ +--- +language: node_js +node_js: + - "4" + +sudo: false + +cache: + directories: + - $HOME/.npm + - $HOME/.cache # includes bowers cache + +before_install: + - npm config set spin false + - npm install -g bower phantomjs-prebuilt + - bower --version + - phantomjs --version + +install: + - npm install + - bower install + +script: + - npm test diff --git a/chrome/.watchmanconfig b/chrome/.watchmanconfig new file mode 100644 index 0000000..e7834e3 --- /dev/null +++ b/chrome/.watchmanconfig @@ -0,0 +1,3 @@ +{ + "ignore_dirs": ["tmp", "dist"] +} diff --git a/chrome/README.md b/chrome/README.md new file mode 100644 index 0000000..c8af9d5 --- /dev/null +++ b/chrome/README.md @@ -0,0 +1,44 @@ +# Huegasm + +This README outlines the details of collaborating on this Ember application. +Music awesomeness for hue lights. + +## Prerequisites + +You will need the following things properly installed on your computer. + +* [Git](http://git-scm.com/) +* [Node.js](http://nodejs.org/) (with NPM) +* [Bower](http://bower.io/) +* [Ember CLI](http://ember-cli.com/) +* [PhantomJS](http://phantomjs.org/) + +## Installation + +* `git clone ` this repository +* `cd huegasm` +* `npm install` +* `bower install` + +## Running / Development + +* `ember serve` +* Visit your app at [http://localhost:4200](http://localhost:4200). + +### Code Generators + +Make use of the many generators for code, try `ember help generate` for more details + +### Building + +* `ember build` (development) +* `ember build --environment production` (production) + +## Further Reading / Useful Links + +* [ember.js](http://emberjs.com/) +* [ember-cli](http://ember-cli.com/) +* Development Browser Extensions + * [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi) + * [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/) + diff --git a/chrome/app/app.js b/chrome/app/app.js new file mode 100644 index 0000000..831ad61 --- /dev/null +++ b/chrome/app/app.js @@ -0,0 +1,18 @@ +import Ember from 'ember'; +import Resolver from './resolver'; +import loadInitializers from 'ember-load-initializers'; +import config from './config/environment'; + +let App; + +Ember.MODEL_FACTORY_INJECTIONS = true; + +App = Ember.Application.extend({ + modulePrefix: config.modulePrefix, + podModulePrefix: config.podModulePrefix, + Resolver +}); + +loadInitializers(App, config.modulePrefix); + +export default App; diff --git a/chrome/app/index.html b/chrome/app/index.html new file mode 100644 index 0000000..297c7f4 --- /dev/null +++ b/chrome/app/index.html @@ -0,0 +1,30 @@ + + + + + + + Huegasm + + + + {{content-for 'head'}} + + + + + {{content-for 'head-footer'}} + + + + + + {{content-for 'body'}} + + + + + {{content-for 'body-footer'}} + + + \ No newline at end of file diff --git a/chrome/app/pods/application/controller.js b/chrome/app/pods/application/controller.js new file mode 100644 index 0000000..77fa65f --- /dev/null +++ b/chrome/app/pods/application/controller.js @@ -0,0 +1,54 @@ +import Ember from 'ember'; + +const { + Controller, + isEmpty, + $ +} = Ember; + +export default Controller.extend({ + dimmerOn: false, + lightsIconsOn: true, + + init(){ + this._super(...arguments); + + let storage = new window.Locally.Store({compress: true}), + dimmerOn = storage.get('huegasm.dimmerOn'), + lightsIconsOn = storage.get('huegasm.lightsIconsOn'); + this.set('storage', storage); + + if (!isEmpty(dimmerOn) && dimmerOn) { + this.send('toggleDimmer'); + } + + if (!isEmpty(lightsIconsOn)) { + this.set('lightsIconsOn', lightsIconsOn); + } + }, + + actions: { + toggleLightsIcons() { + this.toggleProperty('lightsIconsOn'); + + let lightsIconsOn = this.get('lightsIconsOn'); + + this.get('storage').set('huegasm.lightsIconsOn', lightsIconsOn); + }, + toggleDimmer(){ + this.toggleProperty('dimmerOn'); + + let dimmerOn = this.get('dimmerOn'); + + if (dimmerOn) { + $('body').addClass('dimmerOn'); + $('html').addClass('dimmerOn'); + } else { + $('body').removeClass('dimmerOn'); + $('html').removeClass('dimmerOn'); + } + + this.get('storage').set('huegasm.dimmerOn', dimmerOn); + } + } +}); diff --git a/chrome/app/pods/application/template.hbs b/chrome/app/pods/application/template.hbs new file mode 100644 index 0000000..38cb29e --- /dev/null +++ b/chrome/app/pods/application/template.hbs @@ -0,0 +1,3 @@ +{{huegasm-app toggleLightsIcons="toggleLightsIcons" toggleDimmer="toggleDimmer" dimmerOn=dimmerOn lightsIconsOn=lightsIconsOn storage=storage}} + +{{huegasm-footer action="toggleDimmer" dimmerOn=dimmerOn storage=storage}} \ No newline at end of file diff --git a/chrome/app/pods/components/bridge-finder/component.js b/chrome/app/pods/components/bridge-finder/component.js new file mode 100644 index 0000000..4f69cd7 --- /dev/null +++ b/chrome/app/pods/components/bridge-finder/component.js @@ -0,0 +1,152 @@ +import Ember from 'ember'; + +const { + Component, + observer, + computed, + on, + isNone, + run: { later }, + $ +} = Ember; + +export default Component.extend({ + elementId: 'bridge-finder', + classNames: ['container'], + bridgeIp: null, + trial: false, + bridgeUsername: null, + bridgeFindStatus: null, + bridgeFindSuccess: computed.equal('bridgeFindStatus', 'success'), + bridgeFindMultiple: computed.equal('bridgeFindStatus', 'multiple'), + bridgeFindFail: computed.equal('bridgeFindStatus', 'fail'), + bridgeUsernamePingMaxTime: 30000, // 30 seconds + bridgeUsernamePingIntervalTime: 1500, + bridgeUserNamePingIntervalProgress: 0, + bridgePingIntervalHandle: null, + bridgeAuthenticateReachedStatus: null, + manualBridgeIp: null, + manualBridgeIpNotFound: false, + multipleBridgeIps: [], + isAuthenticating: computed.notEmpty('bridgePingIntervalHandle'), + + // try to authenticate against the bridge here + onBridgeIpChange: on('init', observer('bridgeIp', function () { + if (!this.get('trial') && !this.get('isAuthenticating')) { + this.setProperties({ + bridgePingIntervalHandle: setInterval(this.pingBridgeUser.bind(this), this.get('bridgeUsernamePingIntervalTime')), + bridgeUserNamePingIntervalProgress: 0 + }); + } + })), + + didInsertElement() { + $(document).keypress((event) => { + if (!isNone(this.get('manualBridgeIp')) && event.which === 13) { + this.send('findBridgeByIp'); + } + }); + }, + + // find the bridge ip here + init() { + this._super(...arguments); + + if (this.get('bridgeIp') === null) { + $.ajax('https://www.meethue.com/api/nupnp', { + timeout: 30000 + }) + .done((result, status) => { + let bridgeFindStatus = 'fail'; + + if (status === 'success' && result.length === 1) { + this.set('bridgeIp', result[0].internalipaddress); + this.get('storage').set('huegasm.bridgeIp', result[0].internalipaddress); + bridgeFindStatus = 'success'; + } else if (result.length > 1) { + let multipleBridgeIps = this.get('multipleBridgeIps'); + + result.forEach(function (item) { + multipleBridgeIps.pushObject(item.internalipaddress); + }); + + bridgeFindStatus = 'multiple'; + } else { + bridgeFindStatus = 'fail'; + } + + this.set('bridgeFindStatus', bridgeFindStatus); + }) + .fail(() => { + this.set('bridgeFindStatus', 'fail'); + }); + } + }, + + pingBridgeUser() { + let bridgeIp = this.get('bridgeIp'), + bridgeUserNamePingIntervalProgress = this.get('bridgeUserNamePingIntervalProgress'), + bridgeUsernamePingMaxTime = this.get('bridgeUsernamePingMaxTime'); + + if (bridgeIp !== null && bridgeUserNamePingIntervalProgress < 100) { + $.ajax('http://' + bridgeIp + '/api', { + data: JSON.stringify({ "devicetype": "huegasm" }), + contentType: 'application/json', + type: 'POST' + }).done((result, status) => { + if (!this.isDestroyed) { + this.set('bridgeAuthenticateReachedStatus', status); + + if (status === 'success' && !result[0].error) { + this.clearBridgePingIntervalHandle(); + this.get('storage').set('huegasm.bridgeUsername', result[0].success.username); + this.set('bridgeUsername', result[0].success.username); + } + } + }); + + this.incrementProperty('bridgeUserNamePingIntervalProgress', this.get('bridgeUsernamePingIntervalTime') / bridgeUsernamePingMaxTime * 100); + } else { + this.clearBridgePingIntervalHandle(); + } + }, + + clearBridgePingIntervalHandle() { + clearInterval(this.get('bridgePingIntervalHandle')); + this.set('bridgePingIntervalHandle', null); + }, + + actions: { + retry() { + this.onBridgeIpChange(); + }, + chooseBridge(bridge) { + this.set('bridgeIp', bridge); + this.get('storage').set('huegasm.bridgeIp', bridge); + }, + findBridgeByIp() { + let manualBridgeIp = this.get('manualBridgeIp'); + + if (manualBridgeIp.toLowerCase() === 'trial' || manualBridgeIp.toLowerCase() === 'offline') { + this.setProperties({ + trial: true, + bridgeIp: 'trial', + bridgeUsername: 'trial' + }); + } else { + $.ajax('http://' + manualBridgeIp + '/api', { + data: JSON.stringify({ "devicetype": "huegasm" }), + contentType: 'application/json', + type: 'POST' + }).fail(() => { + this.set('manualBridgeIpNotFound', true); + later(this, function () { + this.set('manualBridgeIpNotFound', false); + }, 5000); + }).then(() => { + this.set('bridgeIp', manualBridgeIp); + }); + } + } + }, +}); diff --git a/chrome/app/pods/components/bridge-finder/template.hbs b/chrome/app/pods/components/bridge-finder/template.hbs new file mode 100644 index 0000000..ee0852a --- /dev/null +++ b/chrome/app/pods/components/bridge-finder/template.hbs @@ -0,0 +1,49 @@ +
Huegasm
+{{#unless bridgeUsername}} + {{#if bridgeIp}} + {{paper-progress-linear warn=true value=bridgeUserNamePingIntervalProgress}} + + {{#if isAuthenticating}} +

+ Your bridge IP is {{bridgeIp}} +
Press the button on your bridge to authenticate this application. +

+ {{else}} +

You failed to press the button in time. RETRY

+ {{/if}} + {{else}} + {{#unless bridgeFindStatus}} + {{paper-progress-circular diameter=100}} +

Trying to find your bridge's IP.

+ {{/unless}} + + {{#if bridgeFindMultiple}} +

Huegasm found multiple hue bridges.
Please select the one you want to use for this application.

+ +
+ {{#each multipleBridgeIps as |bridge|}} + {{paper-radio value=bridge label=bridge onChange=(action "chooseBridge")}} + {{/each}} +
+ {{else}} + {{#if bridgeFindFail}} +

A hue bridge could not be automatically found on your network.
Enter one manually?

( or type offline to look around ) +

+ + + {{paper-input label="Hue bridge IP address" value=manualBridgeIp onChange=(action (mut manualBridgeIp))}} + +
+ {{paper-button onClick=(action "findBridgeByIp") raised=true primary=true label="Find"}} +
+
+ + {{#if manualBridgeIpNotFound}} +

+ Could not find a bridge with that IP address. +

+ {{/if}} + {{/if}} + {{/if}} + {{/if}} +{{/unless}} \ No newline at end of file diff --git a/chrome/app/pods/components/hue-controls/component.js b/chrome/app/pods/components/hue-controls/component.js new file mode 100644 index 0000000..a3a5f9f --- /dev/null +++ b/chrome/app/pods/components/hue-controls/component.js @@ -0,0 +1,245 @@ +import Ember from 'ember'; + +const { + A, + Component, + computed, + isEmpty, + isNone, + run: { later, scheduleOnce }, + inject, + $ +} = Ember; + +export default Component.extend({ + classNames: ['container-fluid'], + elementId: 'hue-controls', + lightsData: null, + + activeLights: A(), + tabList: ["Lights", "Music"], + selectedTab: 1, + pauseLightUpdates: false, + + displayFailure: true, + + notify: inject.service(), + + dimmerOnClass: computed('dimmerOn', function () { + return this.get('dimmerOn') ? 'dimmerOn md-menu-origin' : 'md-menu-origin'; + }), + + ready: computed('lightsData', 'trial', function () { + return this.get('trial') || !isNone(this.get('lightsData')); + }), + + apiURL: computed('bridgeIp', 'bridgeUsername', function () { + return 'http://' + this.get('bridgeIp') + '/api/' + this.get('bridgeUsername'); + }), + + tabData: computed('tabList', 'selectedTab', function () { + let tabData = [], selectedTab = this.get('selectedTab'); + + this.get('tabList').forEach(function (tab, i) { + let selected = false; + + if (i === selectedTab) { + selected = true; + } + + tabData.push({ "name": tab, "selected": selected }); + }); + + return tabData; + }), + + didInsertElement() { + // here's a weird way to automatically initialize bootstrap tooltips + let observer = new MutationObserver(function (mutations) { + let haveTooltip = !mutations.every(function (mutation) { + return isEmpty(mutation.addedNodes) || isNone(mutation.addedNodes[0].classList) || mutation.addedNodes[0].classList.contains('tooltip'); + }); + + if (haveTooltip) { + scheduleOnce('afterRender', function () { + $('.bootstrap-tooltip').tooltip(); + }); + } + }); + + observer.observe($('#hue-controls')[0], { childList: true, subtree: true }); + }, + + init() { + this._super(...arguments); + + if (!this.get('trial')) { + this.updateLightData(); + setInterval(this.updateLightData.bind(this), 2000); + } + + if (!isNone(this.get('storage').get('huegasm.selectedTab'))) { + this.set('selectedTab', this.get('storage').get('huegasm.selectedTab')); + } + }, + + updateLightData() { + let fail = () => { + if (isNone(this.get('lightsData'))) { + this.send('clearBridge'); + } else if (this.get('displayFailure')) { + this.get('notify').warning({ html: '' }); + this.set('displayFailure', false); + + later(this, function () { + this.set('displayFailure', true); + }, 30000); + } + }; + + if (!this.get('pauseLightUpdates')) { + $.get(this.get('apiURL') + '/lights', (result, status) => { + if (!isNone(result[0]) && !isNone(result[0].error)) { + fail(); + } else if (status === 'success' && JSON.stringify(this.get('lightsData')) !== JSON.stringify(result)) { + this.set('lightsData', result); + } + }).fail(fail); + } + }, + + actions: { + changeTab(tabName) { + let index = this.get('tabList').indexOf(tabName); + this.set('selectedTab', index); + this.get('storage').set('huegasm.selectedTab', index); + }, + clearBridge() { + let storage = this.get('storage'); + storage.remove('huegasm.bridgeUsername'); + storage.remove('huegasm.bridgeIp'); + location.reload(); + }, + toggleDimmer() { + this.sendAction('toggleDimmer'); + }, + toggleLightsIcons() { + this.sendAction('toggleLightsIcons'); + }, + clearAllSettings() { + this.get('storage').clear(); + location.reload(); + }, + startIntro() { + let intro = introJs(), + playerBottom = $('#player-bottom'); + + if (this.get('dimmerOn')) { + this.send('toggleDimmer'); + } + + intro.setOptions({ + steps: [ + { + intro: 'Welcome! This short tutorial will introduce you to Huegasm.' + }, + { + element: '#music-tab', + intro: 'This is the music player. You\'ll use this to play music and synchronize it with your active lights.

' + + 'TIP: Control which lights are active through the Lights tab.' + }, + { + element: '#playlist', + intro: 'You can add and select music to play from your playlist here. You may listen to local audio files, stream music from soundcloud or stream directly from a connected microphone.

' + + 'TIP: Songs added through Soundcloud will be saved for when you visit this page again.' + }, + { + element: $('#playlist md-menu')[0], + intro: 'You can add songs from SoundCloud by copy and pasting the URL shown here' + }, + { + element: '#player-area', + intro: 'The audio playback may be controlled with the controls here. Basic music visualization effects may be shown here by selecting them from the menu ( eyeball icon in the bottom right ).' + }, + { + element: '#beat-option-row', + intro: 'These are the settings for the music tab:
' + + 'Sensitivity - The sensitivity of the beat detector ( more sensitivity results in more registered beats )
' + + 'Hue Range - The hue range that the lights may change to on beat.
' + + 'Brightness Range - The minimum ( off-beat ) and maximum ( on-beat ) brightness of the lights.
' + + 'Flashing Transitions - Quickly flash the lights on beat
' + + 'Colorloop - Slowly cycle the lights through all the colors while the music is playing
' + + 'TIP: Your sensitivity settings are saved per song as indicated by the red star icon in the top left corner. These settings they will be restored if you ever listen to the same song again.', + position: 'top' + }, + { + element: '#beat-container', + intro: 'An interactive speaker that will bump when a beat is registered.

' + + 'TIP: Click on the center of the speaker to simulate a beat.', + position: 'top' + }, + { + element: '#lights-tab', + intro: 'This is the lights tab. Here you\'ll be able to change various light properties:
' + + 'Power - Turn the selected lights on/off
' + + 'Brightness - The brightness level of the selected lights
' + + 'Color - The color of the selected lights
' + + 'Strobe - Selected lights will flash in sequential order
' + + 'Colorloop - Selected lights will slowly cycle through all the colors
' + }, + { + element: '#active-lights', + intro: 'These icons represent the hue lights in your system. Active lights will be controlled by the application while the inactive lights will have a red X over them and will not be controlled.
' + + 'You may toggle a light\'s state by clicking on it.' + }, + { + element: $('#navigation .ember-basic-dropdown-trigger')[0], + intro: 'A few miscellaneous settings can be found here.

' + + 'WARNING: clearing application settings will restore the application to its original state. This will even delete your playlist and any saved song beat preferences.' + }, + { + intro: 'And that\'s it...Hope you enjoy the application. ;)' + } + ] + }); + + intro.onexit(() => { + $('body').velocity('scroll', { duration: 200 }); + }); + + intro.onchange((element) => { + if (element.id === '' || element.id === 'music-tab' || element.id === 'playlist' || element.id === 'player-area' || element.id === 'beat-option-row' || element.id === 'beat-option-button-group' || element.id === 'beat-container' || element.id === 'using-mic-audio-tooltip' || element.nodeName === 'MD-MENU') { + $('.navigation-item').eq(1).click(); + } else { + $('.navigation-item').eq(0).click(); + } + + if (element.id === 'music-tab' || element.id === 'playlist' || element.id === 'player-area') { + playerBottom.hide(); + } else if (element.id === 'beat-option-row' || element.id === 'beat-option-button-group' || element.id === 'beat-container') { + playerBottom.show(); + } else if (element.id === 'dimmer') { + $(document).click(); + } + }); + + // skip hidden/missing elements + intro.onafterchange((element) => { + let elem = $(element); + if (elem.html() === '') { + $('.introjs-nextbutton').click(); + } + + if (element.id === '') { + later(this, () => { + $('body').velocity('scroll'); + }, 500); + } else { + later(this, () => { + $('.introjs-tooltip').velocity('scroll', { offset: -100 }); + }, 500); + } + }).start(); + } + } +}); diff --git a/chrome/app/pods/components/hue-controls/template.hbs b/chrome/app/pods/components/hue-controls/template.hbs new file mode 100644 index 0000000..8eb84f0 --- /dev/null +++ b/chrome/app/pods/components/hue-controls/template.hbs @@ -0,0 +1,49 @@ +{{#if ready}} + + + {{light-group lightsData=lightsData activeLights=activeLights syncLight=syncLight apiURL=apiURL dimmerOn=dimmerOn lightsIconsOn=lightsIconsOn storage=storage}} + +
+ {{lights-tab active=(eq selectedTab 0) apiURL=apiURL lightsData=lightsData activeLights=activeLights syncLight=syncLight trial=trial colorLoopOn=colorLoopOn dimmerOn=dimmerOn playing=playing pauseLightUpdates=pauseLightUpdates}} + + {{music-tab active=(eq selectedTab 1) apiURL=apiURL lightsData=lightsData activeLights=activeLights pauseLightUpdates=pauseLightUpdates dimmerOn=dimmerOn storage=storage colorLoopOn=colorLoopOn playing=playing action="startIntro"}} +
+{{else}} + {{paper-progress-circular diameter=100}} +{{/if}} + +{{ember-notify messageStyle='bootstrap' closeAfter=5000}} \ No newline at end of file diff --git a/chrome/app/pods/components/huegasm-app/component.js b/chrome/app/pods/components/huegasm-app/component.js new file mode 100644 index 0000000..023d9f6 --- /dev/null +++ b/chrome/app/pods/components/huegasm-app/component.js @@ -0,0 +1,37 @@ +import Ember from 'ember'; + +const { + Component, + isEmpty, + $ +} = Ember; + +export default Component.extend({ + bridgeIp: null, + bridgeUsername: null, + trial: false, + elementId: 'huegasm', + + init() { + this._super(...arguments); + + let storage = this.get('storage'); + + if (!isEmpty(storage.get('huegasm.bridgeIp')) && !isEmpty(storage.get('huegasm.bridgeUsername'))) { + this.setProperties({ + bridgeIp: storage.get('huegasm.bridgeIp'), + bridgeUsername: storage.get('huegasm.bridgeUsername') + }); + } + }, + + actions: { + toggleDimmer() { + this.sendAction('toggleDimmer'); + }, + + toggleLightsIcons() { + this.sendAction('toggleLightsIcons'); + } + } +}); diff --git a/chrome/app/pods/components/huegasm-app/template.hbs b/chrome/app/pods/components/huegasm-app/template.hbs new file mode 100644 index 0000000..41ecb42 --- /dev/null +++ b/chrome/app/pods/components/huegasm-app/template.hbs @@ -0,0 +1,6 @@ +{{#if bridgeUsername}} + {{hue-controls bridgeIp=bridgeIp bridgeUsername=bridgeUsername trial=trial dimmerOn=dimmerOn lightsIconsOn=lightsIconsOn + storage=storage toggleDimmer="toggleDimmer" toggleLightsIcons="toggleLightsIcons"}} +{{else}} + {{bridge-finder bridgeIp=bridgeIp bridgeUsername=bridgeUsername trial=trial storage=storage}} +{{/if}} \ No newline at end of file diff --git a/chrome/app/pods/components/huegasm-footer/component.js b/chrome/app/pods/components/huegasm-footer/component.js new file mode 100644 index 0000000..0d4aa3a --- /dev/null +++ b/chrome/app/pods/components/huegasm-footer/component.js @@ -0,0 +1,21 @@ +import Ember from 'ember'; + +const { + Component, + computed +} = Ember; + +export default Component.extend({ + tagName: 'footer', + classNames: ['footer'], + + year: computed(function(){ + return new Date().getFullYear(); + }), + + actions: { + toggleDimmer(){ + this.sendAction(); + } + } +}); diff --git a/chrome/app/pods/components/huegasm-footer/template.hbs b/chrome/app/pods/components/huegasm-footer/template.hbs new file mode 100644 index 0000000..9ad0fb3 --- /dev/null +++ b/chrome/app/pods/components/huegasm-footer/template.hbs @@ -0,0 +1,13 @@ + + + + + + Get it on the Google Play Store + \ No newline at end of file diff --git a/chrome/app/pods/components/light-group/component.js b/chrome/app/pods/components/light-group/component.js new file mode 100644 index 0000000..3f86d23 --- /dev/null +++ b/chrome/app/pods/components/light-group/component.js @@ -0,0 +1,161 @@ +import Ember from 'ember'; + +const { + A, + Component, + computed, + isEmpty, + isNone, + observer, + $ +} = Ember; + +export default Component.extend({ + elementId: 'active-lights', + classNames: ['light-group'], + isHovering: false, + activeLights: A(), + + // list of all the lights in the hue system + lightsList: computed('lightsData', 'activeLights.[]', 'dimmerOn', function(){ + let lightsData = this.get('lightsData'), + activeLights = this.get('activeLights'), + dimmerOn = this.get('dimmerOn'), + lightsList = A(), + type, + activeClass; + + for (let key in lightsData) { + activeClass = 'light-active'; + + if (lightsData.hasOwnProperty(key) && lightsData[key].state.reachable) { + switch(lightsData[key].modelid){ + case 'LCT001': + type = 'a19'; + break; + case 'LCT002': + type = 'br30'; + break; + case 'LCT003': + type = 'gu10'; + break; + case 'LST001': + type = 'lightstrip'; + break; + case 'LLC010': + type = 'lc_iris'; + break; + case 'LLC011': + type = 'lc_bloom'; + break; + case 'LLC012': + type = 'lc_bloom'; + break; + case 'LLC006': + type = 'lc_iris'; + break; + case 'LLC007': + type = 'lc_aura'; + break; + case 'LLC013': + type = 'storylight'; + break; + case 'LWB004': + type ='a19'; + break; + case 'LLC020': + type = 'huego'; + break; + default: + type = 'a19'; + } + + if(dimmerOn){ + type += 'w'; + } + + if(!activeLights.includes(key)){ + activeClass = 'light-inactive'; + } + + lightsList.push({type: type, name: lightsData[key].name, id: key, data: lightsData[key], activeClass: activeClass}); + } + } + + return lightsList; + }), + + onActiveLightsChange: observer('activeLights.[]', function(){ + this.get('storage').set('huegasm.activeLights', this.get('activeLights')); + }), + + init(){ + this._super(...arguments); + + let lightsData = this.get('lightsData'), + activeLights = this.get('activeLights'), + activeLightsCache = this.get('storage').get('huegasm.activeLights'); + + if(!isNone(activeLightsCache)){ + activeLightsCache.forEach(function(i){ + if (!isNone(lightsData) && lightsData.hasOwnProperty(i) && lightsData[i].state.reachable) { + activeLights.pushObject(i); + } + }); + } else { + for (let key in lightsData) { + if (lightsData.hasOwnProperty(key) && lightsData[key].state.reachable) { + activeLights.pushObject(key); + } + } + } + }, + + actions: { + clickLight(id){ + let activeLights = this.get('activeLights'), + lightId = activeLights.indexOf(id); + + if(lightId !== -1){ + activeLights.removeObject(id); + } else { + activeLights.pushObject(id); + this.set('syncLight', id); + } + }, + lightStartHover(id){ + if(!window.matchMedia || (window.matchMedia("(min-width: 768px)").matches)){ + let hoveredLight = this.get('lightsList').filter(function(light){ + return light.activeClass !== 'unreachable' && light.id === id[0]; + }); + + if(!isEmpty(hoveredLight) && this.get('noHover') !== true){ + $.ajax(this.get('apiURL') + '/lights/' + id + '/state', { + data: JSON.stringify({"alert": "lselect"}), + contentType: 'application/json', + type: 'PUT' + }); + } + + this.set('isHovering', true); + } + }, + lightStopHover(id){ + if(!window.matchMedia || (window.matchMedia("(min-width: 768px)").matches)){ + let hoveredLight = this.get('lightsList').filter(function(light){ + return light.activeClass !== 'unreachable' && light.id === id[0]; + }); + + if(!isEmpty(hoveredLight) && this.get('noHover') !== true){ + $.ajax(this.get('apiURL') + '/lights/' + id + '/state', { + data: JSON.stringify({"alert": "none"}), + contentType: 'application/json', + type: 'PUT' + }); + } + + this.set('isHovering', false); + } + } + } +}); diff --git a/chrome/app/pods/components/light-group/template.hbs b/chrome/app/pods/components/light-group/template.hbs new file mode 100644 index 0000000..3486231 --- /dev/null +++ b/chrome/app/pods/components/light-group/template.hbs @@ -0,0 +1,11 @@ +{{#each lightsList as |light|}} + {{#if lightsIconsOn}} +
+ +
+ {{else}} +
+
{{light.name}}
+
+ {{/if}} +{{/each}} \ No newline at end of file diff --git a/chrome/app/pods/components/lights-tab/color-picker/component.js b/chrome/app/pods/components/lights-tab/color-picker/component.js new file mode 100644 index 0000000..232cfb1 --- /dev/null +++ b/chrome/app/pods/components/lights-tab/color-picker/component.js @@ -0,0 +1,58 @@ +import Ember from 'ember'; + +const { + Component, + $ +} = Ember; + +export default Component.extend({ + elementId: 'color-picker', + rgb: null, + canvas: null, + canvasContext: null, + pressingDown: false, + + mouseUp(){ + this.set('pressingDown', false); + }, + + mouseMove(event){ + if (this.get('pressingDown')) { + this.mouseDown(event); + } + }, + + mouseDown(event){ + let canvasOffset = $(this.get('canvas')).offset(), + canvasX = Math.floor(event.pageX - canvasOffset.left), + canvasY = Math.floor(event.pageY - canvasOffset.top); + + // get current pixel + let imageData = this.get('canvasContext').getImageData(canvasX, canvasY, 1, 1), + pixel = imageData.data; + + this.set('pressingDown', true); + + if (!(pixel[0] === 0 && pixel[1] === 0 && pixel[2] === 0)) { + this.set('rgb', [pixel[0], pixel[1], pixel[2]]); + } + }, + + // https://dzone.com/articles/creating-your-own-html5 + didInsertElement(){ + // handle color changes + let canvas = $('#picker')[0], + canvasContext = canvas.getContext('2d'), + image = new Image(); + + image.src = 'assets/images/colormap.png'; + image.onload = function () { + canvasContext.drawImage(image, 0, 0, image.width, image.height); // draw the image on the canvas + }; + + this.setProperties({ + canvas: canvas, + canvasContext: canvasContext + }); + } +}); diff --git a/chrome/app/pods/components/lights-tab/color-picker/template.hbs b/chrome/app/pods/components/lights-tab/color-picker/template.hbs new file mode 100644 index 0000000..16508e9 --- /dev/null +++ b/chrome/app/pods/components/lights-tab/color-picker/template.hbs @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/chrome/app/pods/components/lights-tab/component.js b/chrome/app/pods/components/lights-tab/component.js new file mode 100644 index 0000000..5086f2c --- /dev/null +++ b/chrome/app/pods/components/lights-tab/component.js @@ -0,0 +1,348 @@ +import Ember from 'ember'; + +const { + Component, + observer, + computed, + on, + run: { later, once }, + $ +} = Ember; + +export default Component.extend({ + classNames: ['col-sm-10', 'col-sm-offset-1', 'col-xs-12'], + classNameBindings: ['active::hidden'], + elementId: 'lights-tab', + + rgb: [255, 255, 255], + + lightsOn: false, + + // COLOR LOOP related stuff + colorLoopOn: false, + + lightsOnTxt: computed('lightsOn', function () { + return this.get('lightsOn') ? 'On' : 'Off'; + }), + + colorloopOnTxt: computed('colorLoopOn', function () { + return this.get('colorLoopOn') ? 'On' : 'Off'; + }), + + // determines the average brightness of the hue system for the brightness slider + lightsBrightness: computed('lightsData', function () { + let lightsData = this.get('lightsData'), + activeLights = this.get('activeLights'), + lightsBrightness = 0; + + activeLights.forEach(function (light) { + lightsBrightness += lightsData[light].state.bri; + }); + + return lightsBrightness / activeLights.length; + }), + + brightnessControlDisabled: computed.not('lightsOn'), + + onColorLoopOnChange: observer('colorLoopOn', function () { + let lightsData = this.get('lightsData'), + activeLights = this.get('activeLights'), + colorLoopsOn = this.get('colorLoopOn'), + effect = colorLoopsOn ? 'colorloop' : 'none'; + + let colorLoopsOnSystem = activeLights.some(function (light) { + return lightsData[light].state.effect === 'colorloop'; + }); + + // if the internal lights state is different than the one from lightsData ( user manually toggled the switch ), send the request to change the bulbs state + if (colorLoopsOn !== colorLoopsOnSystem) { + activeLights.forEach((light) => { + if (this.get('lightsData')[light].state.effect !== effect) { + $.ajax(this.get('apiURL') + '/lights/' + light + '/state', { + data: JSON.stringify({ 'effect': effect }), + contentType: 'application/json', + type: 'PUT' + }); + } + }); + } + }), + + rgbPreview: observer('rgb', function () { + let rgb = this.get('rgb'), + xy = this.rgbToXy(rgb[0], rgb[1], rgb[2]); + + this.set('colorLoopOn', false); + + this.get('activeLights').forEach((light) => { + $.ajax(this.get('apiURL') + '/lights/' + light + '/state', { + data: JSON.stringify({ "xy": xy }), + contentType: 'application/json', + type: 'PUT' + }); + }); + + this.set('colorLoopOn', false); + $('.color').css('background', 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')'); + }), + + // determines whether the lights are on/off for the lights switch + lightsOnChange: on('init', observer('lightsData.@each.state.on', 'activeLights.[]', function () { + if (!this.get('strobeOn')) { + let lightsData = this.get('lightsData'), lightsOn = this.get('activeLights').some(function (light) { + return lightsData[light].state.on === true; + }); + + this.set('lightsOn', lightsOn); + } + })), + + onLightsOnChange: observer('lightsOn', function () { + let lightsData = this.get('lightsData'), + activeLights = this.get('activeLights'), + lightsOn = this.get('lightsOn'); + + let lightsOnSystem = activeLights.some(function (light) { + return lightsData[light].state.on === true; + }); + + // if the internal lights state is different than the one from lightsData ( user manually toggled the switch ), send the request to change the bulbs state + if (lightsOn !== lightsOnSystem) { + activeLights.forEach((light) => { + $.ajax(this.get('apiURL') + '/lights/' + light + '/state', { + data: JSON.stringify({ "on": lightsOn }), + contentType: 'application/json', + type: 'PUT' + }); + }); + } + }), + + onBrightnessChanged: observer('lightsBrightness', function () { + once(this, function () { + let lightsData = this.get('lightsData'), + lightsBrightnessSystem = false, + lightsBrightness = this.get('lightsBrightness'), + activeLights = this.get('activeLights'); + + activeLights.forEach(function (light) { + lightsBrightnessSystem += lightsData[light].state.bri; + }); + + lightsBrightnessSystem /= activeLights.length; + + // if the internal lights state is different than the one from lightsData ( user manually toggled the switch ), send the request to change the bulbs state + if (lightsBrightness !== lightsBrightnessSystem) { + activeLights.forEach((light) => { + $.ajax(this.get('apiURL') + '/lights/' + light + '/state', { + data: JSON.stringify({ "bri": lightsBrightness }), + contentType: 'application/json', + type: 'PUT' + }); + }); + } + }); + }), + + // sync the current light settings to the newly added light + onaActiveLightsChange: observer('syncLight', function () { + let options = { + on: this.get('lightsOn'), + bri: this.get('lightsBrightness'), + effect: this.get('colorLoopOn') ? 'colorloop' : 'none' + }, rgb = this.get('rgb'), + syncLight = this.get('syncLight'); + + if (rgb[0] !== 255 && rgb[1] !== 255 && rgb[2] !== 255) { + options['xy'] = this.rgbToXy(rgb[0], rgb[1], rgb[2]); + } + + options['transitiontime'] = 0; + + $.ajax(this.get('apiURL') + '/lights/' + syncLight + '/state', { + data: JSON.stringify(options), + contentType: 'application/json', + type: 'PUT' + }); + }), + + // **************** STROBE LIGHT START **************** + strobeOn: false, + + strobeOnInervalHandle: null, + preStrobeOnLightsDataCache: null, + nextLightIdx: 0, + + onStrobeOnChange: observer('strobeOn', function () { + let lightsData = this.get('lightsData'), + strobeOn = this.get('strobeOn'); + + if (strobeOn) { + this.set('preStrobeOnLightsDataCache', lightsData); + let stobeInitRequestData = { transitiontime: 0 }; + + for (let key in lightsData) { + if (lightsData.hasOwnProperty(key)) { + if (lightsData[key].state.on) { + stobeInitRequestData.on = false; + } + + $.ajax(this.get('apiURL') + '/lights/' + key + '/state', { + data: JSON.stringify(stobeInitRequestData), + contentType: 'application/json', + type: 'PUT' + }); + } + } + + this.set('strobeOnInervalHandle', setInterval(this.strobeStep.bind(this), 500)); + } else { // revert the light system to pre-strobe + let preStrobeOnLightsDataCache = this.get('preStrobeOnLightsDataCache'), updateLight = (lightIndex) => { + $.ajax(this.get('apiURL') + '/lights/' + lightIndex + '/state', { + data: JSON.stringify({ + on: preStrobeOnLightsDataCache[lightIndex].state.on, + sat: preStrobeOnLightsDataCache[lightIndex].state.sat + }), + contentType: 'application/json', + type: 'PUT' + }); + }; + + for (let key in lightsData) { + if (lightsData.hasOwnProperty(key)) { + later(this, updateLight, key, 2000); + } + } + + later(this, this.onColorLoopOnChange, 2000); + clearInterval(this.get('strobeOnInervalHandle')); + } + + this.set('pauseLightUpdates', strobeOn); + }), + + strobeStep() { + let nextLightIdx = this.get('nextLightIdx') % this.get('activeLights').length, + nextStrobeLight = this.get('activeLights')[nextLightIdx], + turnOnOptions = { on: true, transitiontime: 0, alert: 'select' }; + + // random light if in cololoop mode + if (this.get('colorLoopOn')) { + turnOnOptions.hue = Math.floor(Math.random() * 65535); + } + + $.ajax(this.get('apiURL') + '/lights/' + nextStrobeLight + '/state', { + data: JSON.stringify(turnOnOptions), + contentType: 'application/json', + type: 'PUT' + }); + $.ajax(this.get('apiURL') + '/lights/' + nextStrobeLight + '/state', { + data: JSON.stringify({ 'on': false, 'transitiontime': 0 }), + contentType: 'application/json', + type: 'PUT' + }); + + this.set('nextLightIdx', ++nextLightIdx); + }, + + strobeOnTxt: computed('strobeOn', function () { + return this.get('strobeOn') ? 'On' : 'Off'; + }), + + dimmerOnClass: computed('dimmerOn', function () { + return this.get('dimmerOn') ? 'dimmerOn' : null; + }), + + actions: { + toggleDimmer() { + this.sendAction('toggleDimmer'); + } + }, + + // **************** STROBE LIGHT FINISH **************** + // http://www.developers.meethue.com/documentation/color-conversions-rgb-xy + rgbToXy(red, green, blue) { + let X, Y, Z, x, y; + + // normalize + red = Number((red / 255)); + green = Number((green / 255)); + blue = Number((blue / 255)); + + // gamma correction + red = (red > 0.04045) ? Math.pow((red + 0.055) / (1.0 + 0.055), 2.4) : (red / 12.92); + green = (green > 0.04045) ? Math.pow((green + 0.055) / (1.0 + 0.055), 2.4) : (green / 12.92); + blue = (blue > 0.04045) ? Math.pow((blue + 0.055) / (1.0 + 0.055), 2.4) : (blue / 12.92); + + // RGB to XYZ + X = red * 0.664511 + green * 0.154324 + blue * 0.162028; + Y = red * 0.283881 + green * 0.668433 + blue * 0.047685; + Z = red * 0.000088 + green * 0.072310 + blue * 0.986039; + + x = X / (X + Y + Z); + y = Y / (X + Y + Z); + + return [x, y]; + }, + + xyToRgb(x, y) { + let r, g, b, X, Y = 1.0, Z; + + X = (Y / y) * x; + Z = (Y / y) * (1 - x - y); + + r = X * 1.656492 - Y * 0.354851 - Z * 0.255038; + g = X * -0.707196 + Y * 1.655397 + Z * 0.036152; + b = X * 0.051713 - Y * 0.121364 + Z * 1.011530; + + if (r > b && r > g && r > 1.0) { + // red is too big + g = g / r; + b = b / r; + r = 1.0; + } else if (g > b && g > r && g > 1.0) { + // green is too big + r = r / g; + b = b / g; + g = 1.0; + } else if (b > r && b > g && b > 1.0) { + // blue is too big + r = r / b; + g = g / b; + b = 1.0; + } + + r = (r <= 0.0031308) ? 12.92 * r : 1.055 * Math.pow(r, (1.0 / 2.4)) - 0.055; + g = (g <= 0.0031308) ? 12.92 * g : 1.055 * Math.pow(g, (1.0 / 2.4)) - 0.055; + b = (b <= 0.0031308) ? 12.92 * b : 1.055 * Math.pow(b, (1.0 / 2.4)) - 0.055; + + if (r > b && r > g) { + // red is biggest + if (r > 1.0) { + g = g / r; + b = b / r; + r = 1.0; + } + } else if (g > b && g > r) { + // green is biggest + if (g > 1.0) { + r = r / g; + b = b / g; + g = 1.0; + } + } else if (b > r && b > g) { + // blue is biggest + if (b > 1.0) { + r = r / b; + g = g / b; + b = 1.0; + } + } + + r = r * 255; + g = g * 255; + b = b * 255; + + return [r, g, b]; + } +}); diff --git a/chrome/app/pods/components/lights-tab/template.hbs b/chrome/app/pods/components/lights-tab/template.hbs new file mode 100644 index 0000000..8b68b7e --- /dev/null +++ b/chrome/app/pods/components/lights-tab/template.hbs @@ -0,0 +1,43 @@ +{{#paper-list}} + {{#paper-item}} + {{paper-icon "power-settings-new" class=dimmerOnClass}} +

Power

+ {{paper-switch value=lightsOn onChange=(action (mut lightsOn)) disabled=(or trial playing) skipProxy=trial label=lightsOnTxt}} + {{/paper-item}} + + {{#paper-item}} + {{paper-icon "brightness-4" class=dimmerOnClass}} +

Brightness

+ {{paper-slider class="flex" step=10 min=1 max=254 value=lightsBrightness onChange=(action (mut lightsBrightness)) disabled=brightnessControlDisabled}} + {{/paper-item}} + + {{#paper-item elementId="color-row" }} + {{paper-icon "color-lens" class=dimmerOnClass}} +

Color

+ + {{#paper-menu offset="0 -50" as |menu|}} + {{#menu.trigger}} + {{#paper-button iconButton=false}} + {{paper-button raised=true class="color" disabled=(or trial playing)}} + {{/paper-button}} + {{/menu.trigger}} + {{#menu.content class="color-content" width=0 as |content|}} + {{#content.menu-item}} + {{lights-tab/color-picker lightsData=lightsData activeLights=activeLights rgb=rgb}} + {{/content.menu-item}} + {{/menu.content}} + {{/paper-menu}} + {{/paper-item}} + + {{#paper-item}} + {{paper-icon "flare" class=dimmerOnClass}} +

Strobe

+ {{paper-switch value=strobeOn onChange=(action (mut strobeOn)) disabled=(or trial playing) skipProxy=trial label=strobeOnTxt}} + {{/paper-item}} + + {{#paper-item}} + {{paper-icon "color-lens" class=dimmerOnClass}} {{paper-icon "loop" id="loop-addition" class=dimmerOnClass}} +

Colorloop

+ {{paper-switch value=colorLoopOn onChange=(action (mut colorLoopOn)) disabled=(or trial playing) skipProxy=trial label=colorloopOnTxt}} + {{/paper-item}} +{{/paper-list}} \ No newline at end of file diff --git a/chrome/app/pods/components/music-tab/add-soundcloud-sound-modal/component.js b/chrome/app/pods/components/music-tab/add-soundcloud-sound-modal/component.js new file mode 100644 index 0000000..072e4a9 --- /dev/null +++ b/chrome/app/pods/components/music-tab/add-soundcloud-sound-modal/component.js @@ -0,0 +1,46 @@ +import Ember from 'ember'; + +const { + Component, + observer, + computed, + isEmpty, + isNone, + run: { later }, + $ +} = Ember; + +export default Component.extend({ + url: null, + + onIsShowingModalChange: observer('isShowingModal', function(){ + if(this.get('isShowingModal')){ + this.set('url', null); + later(function(){ + $('md-input-container input').focus(); + }, 500); + } + + }), + + saveDisabled: computed('url', function(){ + return isNone(this.get('url')) || isEmpty(this.get('url').trim()); + }), + + didInsertElement: function() { + $(document).keypress((event)=>{ + if(!this.get('saveDisabled') && event.which === 13) { + this.send('add'); + } + }); + }, + + actions: { + close () { + this.sendAction(); + }, + add (){ + this.sendAction('action', this.get('url')); + } + } +}); diff --git a/chrome/app/pods/components/music-tab/add-soundcloud-sound-modal/template.hbs b/chrome/app/pods/components/music-tab/add-soundcloud-sound-modal/template.hbs new file mode 100644 index 0000000..d889152 --- /dev/null +++ b/chrome/app/pods/components/music-tab/add-soundcloud-sound-modal/template.hbs @@ -0,0 +1,14 @@ +{{#if isShowingModal}} + {{#modal-dialog close="close" alignment="center" translucentOverlay=true attachment="center" targetAttachment="center"}} + +

Enter a SoundCloud track or playlist/set URL

+

( ex. https://soundcloud.com/mrsuicidesheep/tracks )

+ + {{paper-input label="SoundCloud URL" icon="search" value=url onChange=(action (mut url))}} + +
+ {{paper-button onClick=(action "close") label="Close"}} + {{paper-button class="pull-right" onClick=(action "add") disabled=saveDisabled primary=true label="Add Music"}} +
+ {{/modal-dialog}} +{{/if}} \ No newline at end of file diff --git a/chrome/app/pods/components/music-tab/component.js b/chrome/app/pods/components/music-tab/component.js new file mode 100644 index 0000000..92dc0b4 --- /dev/null +++ b/chrome/app/pods/components/music-tab/component.js @@ -0,0 +1,737 @@ +import Ember from 'ember'; +import helperMixin from './mixins/helpers'; +import visualizerMixin from './mixins/visualizer'; + +const { + Component, + observer, + isEmpty, + isNone, + $, + run: { later, next } +} = Ember; + +export default Component.extend(helperMixin, visualizerMixin, { + updatePageTitle: observer('playQueuePointer', function () { + let title = 'Huegasm', + playQueuePointer = this.get('playQueuePointer'), + playQueue = this.get('playQueue'); + + if (playQueuePointer !== -1) { + let song = playQueue[playQueuePointer]; + if (song.title) { + title = song.title; + + if (song.artist) { + title += (' - ' + song.artist); + } + } else { + title = song.fileName; + } + + title += '- Huegasm'; + } + + document.title = title; + }), + + changePlayerControl(name, value, saveBeatPrefs) { + this.set(name, value); + + if (name === 'threshold') { + this.get('kick').set({ threshold: value }); + } + + if (saveBeatPrefs && this.get('playQueuePointer') !== -1) { + this.saveSongBeatPreferences(); + } + + this.get('storage').set('huegasm.' + name, value); + }, + + saveSongBeatPreferences() { + let song = this.get('playQueue')[this.get('playQueuePointer')]; + if (song) { + let title = isEmpty(song.artist) ? song.fileName : song.artist + '-' + song.title, + songBeatPreferences = this.get('songBeatPreferences'); + + songBeatPreferences[title] = { threshold: this.get('threshold') }; + + this.set('usingBeatPreferences', true); + this.get('storage').set('huegasm.songBeatPreferences', songBeatPreferences); + } + }, + + loadSongBeatPreferences() { + let song = this.get('playQueue')[this.get('playQueuePointer')], + title = isEmpty(song.artist) ? song.fileName : song.artist + '-' + song.title, + songBeatPreferences = this.get('songBeatPreferences'), + preference = songBeatPreferences[title], + oldBeatPrefCache = this.get('oldBeatPrefCache'), + newOldBeatPrefCache = null; + + if (!isNone(preference)) { // load existing beat prefs + newOldBeatPrefCache = { threshold: this.get('threshold') }; + + this.changePlayerControl('threshold', preference.threshold); + this.set('usingBeatPreferences', true); + } else if (!isNone(oldBeatPrefCache)) { // revert to using beat prefs before the remembered song + this.changePlayerControl('threshold', oldBeatPrefCache.threshold); + this.set('usingBeatPreferences', false); + } + + this.set('oldBeatPrefCache', newOldBeatPrefCache); + }, + + clearCurrentAudio(resetPointer) { + let dancer = this.get('dancer'); + + if (dancer.audio.pause) { + dancer.pause(); + } + + if (resetPointer) { + this.set('playQueuePointer', -1); + } + + this.setProperties({ + timeElapsed: 0, + timeTotal: 0, + playing: false + }); + }, + + dragOver() { + let dragLeaveTimeoutHandle = this.get('dragLeaveTimeoutHandle'); + this.set('dragging', true); + + if (dragLeaveTimeoutHandle) { + clearTimeout(dragLeaveTimeoutHandle); + } + }, + + dragLeave() { + // need to delay the dragLeave notification to avoid flickering ( hovering over some page elements causes this event to be sent ) + this.set('dragLeaveTimeoutHandle', setTimeout(() => { this.set('dragging', false); }, 500)); + }, + + simulateKick(/*mag, ratioKickMag*/) { + let activeLights = this.get('activeLights'), + lightsData = this.get('lightsData'), + color = null, + + transitiontime = this.get('flashingTransitions'), + stimulateLight = (light, brightness, hue) => { + let options = { 'bri': brightness }; + + if (transitiontime) { + options['transitiontime'] = 0; + } else { + options['transitiontime'] = 1; + } + + if (!isNone(hue)) { + options.hue = hue; + } + + if (lightsData[light].state.on === false) { + options.on = true; + } + + $.ajax(this.get('apiURL') + '/lights/' + light + '/state', { + data: JSON.stringify(options), + contentType: 'application/json', + type: 'PUT' + }); + }, + timeToBriOff = 100; + + if (activeLights.length > 0) { + let lastLightBopIndex = this.get('lastLightBopIndex'), + lightBopIndex, + brightnessRange = this.get('brightnessRange'), + light; + + lightBopIndex = Math.floor(Math.random() * activeLights.length); + + // let's try not to select the same light twice in a row + if (activeLights.length > 1) { + while (lightBopIndex === lastLightBopIndex) { + lightBopIndex = Math.floor(Math.random() * activeLights.length); + } + } + + light = activeLights[lightBopIndex]; + this.set('lastLightBopIndex', lightBopIndex); + + if (!this.get('colorloopMode')) { + let hueRange = this.get('hueRange'); + + color = Math.floor(Math.random() * (hueRange[1] - hueRange[0] + 1) + hueRange[0]); + } + + if (transitiontime) { + timeToBriOff = 80; + } + + stimulateLight(light, brightnessRange[1], color); + later(this, stimulateLight, light, brightnessRange[0], timeToBriOff); + } + + this.set('paused', true); + later(this, function () { + this.set('paused', false); + }, 150); + + //work the music beat area - simulate the speaker vibration by running a CSS animation on it + $('#beat-speaker-center-outer').velocity({ blur: 3 }, 100).velocity({ blur: 0 }, 100); + $('#beat-speaker-center-inner').velocity({ scale: 1.05 }, 100).velocity({ scale: 1 }, 100); + }, + + init() { + this._super(...arguments); + + window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame; + window.cancelAnimationFrame = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.msCancelAnimationFrame; + navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; + + let dancer = new Dancer(), + storage = this.get('storage'), + kick = dancer.createKick({ + threshold: this.get('threshold'), + onKick: (mag, ratioKickMag) => { + if (this.get('paused') === false) { + this.simulateKick(mag, ratioKickMag); + } + } + }); + + kick.on(); + + this.setProperties({ + dancer: dancer, + kick: kick + }); + + ['volume', 'shuffle', 'repeat', 'volumeMuted', 'threshold', 'playerBottomDisplayed', 'songBeatPreferences', 'firstVisit', 'currentVisName', 'playQueue', 'playQueuePointer', 'flashingTransitions', 'colorloopMode', 'hueRange', 'brightnessRange'].forEach((item) => { + if (!isNone(storage.get('huegasm.' + item))) { + let itemVal = storage.get('huegasm.' + item); + + if (isNone(this.actions[item + 'Changed'])) { + this.set(item, itemVal); + } else { + this.send(item + 'Changed', itemVal); + } + } + }); + + this.set('oldPlayQueueLength', this.get('playQueue.length')); + + SC.initialize({ + client_id: this.get('SC_CLIENT_ID') + }); + }, + + didInsertElement() { + this._super(); + + let self = this; + + // file input code + $('#file-input').on('change', function () { + let files = this.files; + self.send('handleNewFiles', files); + this.value = null; // reset in case upload the second file again + }); + + $(document).on('click', '.alert', (event) => { + $(event.target).addClass('removed'); + }); + + // prevent space/text selection when the user repeatedly clicks on the center + $('#beat-container').on('mousedown', '#beat-speaker-center-inner', function (event) { + event.preventDefault(); + }); + + $(document).keypress((event) => { + if (event.which === 32 && event.target.type !== 'text') { + this.send('play'); + } + }); + + this.$().on('drop', '#play-list-area', (event) => { + this.send('dropFiles', event.dataTransfer.files); + }); + + // control the volume by scrolling up/down + $('#player-area').on('mousewheel', (event) => { + if (this.get('playQueueNotEmpty')) { + let scrollSize = 5; + + if (event.deltaY < 0) { + scrollSize *= -1; + } + let newVolume = this.get('volume') + scrollSize; + + this.send('volumeChanged', newVolume < 0 ? 0 : newVolume); + event.preventDefault(); + } + }); + + // demo tracks + if (this.get('firstVisit')) { + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/candyland-speechless-feat-rkcb'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/dillistone/dillistone-lili-n-rude'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/vallis-alps-young-feki-remix'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/andrew-luce-when-to-love-you-feat-chelsea-cutler'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/ahh-ooh-carefree-with-me'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/crywolf-slow-burn'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/clozee-red-forest'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/elo-method-subranger-solace'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/90-pounds-of-pete-waited-too-long-feat-devon-baldwin'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/mrsuicidesheep/draper-eyes-open'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/itspapaya/sunny'); + this.send('handleNewSoundCloudURL', 'https://soundcloud.com/stonesthrow/nxworries-anderson-paak-knxwledge-suede'); + + this.get('storage').set('huegasm.firstVisit', false); + + this.sendAction(); + } + + if (!this.get('playerBottomDisplayed')) { + $('#player-bottom').hide(); + } + }, + + actions: { + clearPlaylist() { + this.get('playQueue').clear(); + }, + setVisName(name) { + this.set('currentVisName', name); + }, + hideTooltip() { + $('.bootstrap-tooltip').tooltip('hide'); + }, + gotoSCURL(URL) { + // need to pause the music since soundcloud is going to start playing this song anyways + if (this.get('playing')) { + this.send('play'); + } + + this.send('gotoURL', URL); + }, + gotoURL(URL) { + $('.tooltip').remove(); + window.open(URL, '_blank'); + }, + handleNewSoundCloudURL(URL) { + if (URL) { + SC.resolve(URL).then((resultObj) => { + let processResult = (result) => { + if (result.kind === 'user') { + this.get('notify').alert({ html: this.get('scUserNotSupportedHtml') }); + } else if (result.kind === 'track') { + if (result.streamable === true) { + let picture = null; + + if (result.artwork_url) { + picture = result.artwork_url.replace('large', 't67x67'); + } else if (result.user.avatar_url) { + picture = result.user.avatar_url; + } + + $.get(picture) + .done(() => { + this.get('playQueue').pushObject({ url: result.stream_url + '?client_id=' + this.get('SC_CLIENT_ID'), fileName: result.title + ' - ' + result.user.username, artist: result.user.username, scUrl: result.permalink_url, title: result.title, picture: picture }); + }).fail(() => { // no picture + this.get('playQueue').pushObject({ url: result.stream_url + '?client_id=' + this.get('SC_CLIENT_ID'), fileName: result.title + ' - ' + result.user.username, artist: result.user.username, scUrl: result.permalink_url, title: result.title }); + }); + } else { + failedSongs.push(result.title); + } + } else if (result.kind === 'playlist') { + if (result.streamable === true) { + result.tracks.forEach(processResult); + } else { + failedSongs.push(result.title); + } + } + }, + failedSongs = []; + + if (resultObj instanceof Array) { + resultObj.forEach(processResult); + } else { + processResult(resultObj); + } + + if (failedSongs.length > 0) { + this.get('notify').alert({ html: this.get('notStreamableHtml')(failedSongs) }); + } + + if (this.get('playQueuePointer') === -1) { + if (this.get('firstVisit')) { + this.send('goToSong', 0); + } else { + this.send('next'); + } + } + }, () => { + this.get('notify').alert({ html: this.get('urlNotFoundHtml')(URL) }); + }); + } + + this.set('isShowingAddSoundCloudModal', false); + }, + toggleIsShowingAddSoundCloudModal() { + this.toggleProperty('isShowingAddSoundCloudModal'); + }, + slideTogglePlayerBottom() { + let elem = this.$('#player-bottom'); + + elem.velocity(elem.is(':visible') ? 'slideUp' : 'slideDown', { duration: 300 }); + this.changePlayerControl('playerBottomDisplayed', !this.get('playerBottomDisplayed')); + }, + goToSong(index, playSong, scrollToSong) { + let dancer = this.get('dancer'), playQueue = this.get('playQueue'); + + if (dancer.audio) { + this.clearCurrentAudio(true); + } + + if (!isNone(playQueue[index])) { + let audio = new Audio(); + audio.src = this.get('playQueue')[index].url; + + audio.crossOrigin = "anonymous"; + audio.oncanplay = () => { + this.set('timeTotal', Math.floor(audio.duration)); + this.set('soundCloudFuckUps', 0); + }; + audio.onerror = (event) => { + let playQueuePointer = this.get('playQueuePointer'), + song = this.get('playQueue')[playQueuePointer]; + + if (this.get('soundCloudFuckUps') >= this.get('maxSoundCloudFuckUps')) { + this.get('notify').alert({ html: this.get('tooManySoundCloudFuckUps') }); + this.send('play'); + this.set('soundCloudFuckUps', 0); + } else { + if (song.local) { + this.send('removeAudio', playQueuePointer); + } else { + this.send('next', true); + } + + if (event.target.error.code === 2) { + this.get('notify').alert({ html: this.get('failedToDecodeFileHtml')(song.fileName) }); + } else { + this.get('notify').alert({ html: this.get('failedToPlayFileHtml')(song.fileName) }); + } + + this.set('usingBeatPreferences', false); + this.incrementProperty('soundCloudFuckUps'); + } + }; + audio.ontimeupdate = () => { + this.set('timeElapsed', Math.floor(audio.currentTime)); + }; + audio.onended = () => { + this.send('next'); + }; + + dancer.load(audio, 1); + + this.set('playQueuePointer', index); + + this.loadSongBeatPreferences(); + + if (playSong) { + this.send('play'); + } + + if (scrollToSong) { + // this is just a bad workaround to make sure that the track has been rendered to the playlist + next(this, () => { + $('.track' + index).velocity('scroll', { container: $('#play-list-area'), duration: 200 }); + }); + } + } + }, + removeAudio(index) { + this.get('playQueue').removeAt(index); + + // need to manually remove the tooltip + $('body .tooltip').remove(); + + if (index === this.get('playQueuePointer')) { + this.send('goToSong', index, true, true); + } + }, + playerAreaPlay() { + if (isEmpty($('#player-controls:hover')) && this.get('playQueuePointer') !== -1) { + this.send('play'); + + $('#play-notification').velocity({ opacity: 0.8, scale: 1 }, 0).velocity({ opacity: 0, scale: 3 }, 500); + } + }, + play(replayPause) { + let dancer = this.get('dancer'), + playQueuePointer = this.get('playQueuePointer'), + playing = this.get('playing'), + lightsData = this.get('lightsData'); + + if (playQueuePointer !== -1) { + if (playing) { + dancer.pause(); + + let preMusicLightsDataCache = this.get('preMusicLightsDataCache'), + updateLight = (lightIndex) => { + $.ajax(this.get('apiURL') + '/lights/' + lightIndex + '/state', { + data: JSON.stringify({ + 'on': preMusicLightsDataCache[lightIndex].state.on, + 'hue': preMusicLightsDataCache[lightIndex].state.hue, + 'bri': preMusicLightsDataCache[lightIndex].state.bri + }), + contentType: 'application/json', + type: 'PUT' + }); + }; + + for (let key in lightsData) { + if (lightsData.hasOwnProperty(key)) { + later(this, updateLight, key, 1000); + } + } + + if (!replayPause) { + this.set('timeElapsed', Math.floor(dancer.getTime())); + } + } else { + let timeTotal = this.get('timeTotal'); + + if (this.get('volumeMuted')) { + dancer.setVolume(0); + } else { + dancer.setVolume(this.get('volume') / 100); + } + + // replay song + if (this.get('timeElapsed') === timeTotal && timeTotal !== 0) { + this.send('next', true); + return; + } + + $(window).trigger('resize'); // workaround to redraw the canvas for the vitualizer + + this.set('preMusicLightsDataCache', lightsData); + dancer.play(); + } + + this.set('pauseLightUpdates', !playing); + this.onColorloopModeChange(); + this.toggleProperty('playing'); + } + }, + volumeChanged(value) { + this.changePlayerControl('volume', value); + if (this.get('playing')) { + this.get('dancer').setVolume(value / 100); + } + + if (this.get('volume') > 0 && this.get('volumeMuted')) { + this.changePlayerControl('volumeMuted', false); + } + }, + next(repeatAll) { + let playQueuePointer = this.get('playQueuePointer'), + playQueue = this.get('playQueue'), + nextSong = (playQueuePointer + 1), + repeat = this.get('repeat'), + shuffle = this.get('shuffle'); + + if (repeat === 2) { // repeating one song takes precedence over shuffling + if (playQueuePointer === -1 && playQueue.length > 0) { + nextSong = 0; + } else { + nextSong = playQueuePointer; + } + } else if (shuffle) { // next shuffle song + let shufflePlayed = this.get('shufflePlayed'); + + // played all the song in shuffle mode + if (shufflePlayed.length === playQueue.length) { + shufflePlayed.clear(); + this.send('play', true); + return; + } + + // we're going to assume that the song URL is the id + do { + nextSong = Math.floor(Math.random() * playQueue.length); + } while (shufflePlayed.includes(playQueue[nextSong].url)); + + shufflePlayed.pushObject(playQueue[nextSong].url); + } else if (nextSong > playQueue.length - 1) { + if (repeat === 1 || repeatAll) { + nextSong = nextSong % playQueue.length; + } else { + this.send('play', true); + return; + } + } + + this.send('goToSong', nextSong, true, true); + }, + previous() { + if (this.get('timeElapsed') > 5) { + this.send('seekChanged', 0); + } else { + let nextSong = this.get('playQueuePointer'), + playQueue = this.get('playQueue'); + + if (this.get('shuffle') && !isNone(playQueue[nextSong])) { // go to the previously shuffled song + let shufflePlayed = this.get('shufflePlayed'), + shuffledSongIndx = this.get('shufflePlayed').indexOf(playQueue[nextSong].url), + i = 0; + + if (shufflePlayed.length > 0 && shuffledSongIndx !== -1) { // only if there was one + nextSong = shuffledSongIndx - 1; + + if (nextSong < 0) { + nextSong = shufflePlayed.length - 1; + } + + playQueue.some(function (item) { // try to find the previous song id + if (item.url === shufflePlayed[nextSong]) { + nextSong = i; + return true; + } + i++; + + return false; + }); + } + } else { + nextSong--; + + if (nextSong < 0) { + nextSong = playQueue.length - 1; + } + } + + this.send('goToSong', nextSong, true, true); + } + }, + seekChanged(position) { + let dancer = this.get('dancer'); + + if (dancer.audio) { + dancer.audio.currentTime = Math.floor(this.get('timeTotal') * position / 100); + } + }, + volumeMutedChanged(value) { + let dancer = this.get('dancer'), + volumeMuted = isNone(value) ? !this.get('volumeMuted') : value; + + this.changePlayerControl('volumeMuted', volumeMuted); + + if (this.get('playing')) { + if (volumeMuted) { + dancer.setVolume(0); + } else { + dancer.setVolume(this.get('volume') / 100); + } + } + }, + addLocalAudio: function () { + $('#file-input').click(); + }, + shuffleChanged(value) { + this.changePlayerControl('shuffle', isNone(value) ? !this.get('shuffle') : value); + }, + repeatChanged(value) { + this.changePlayerControl('repeat', isNone(value) ? (this.get('repeat') + 1) % 3 : value); + }, + playerBottomDisplayedChanged(value) { + this.changePlayerControl('playerBottomDisplayed', value); + }, + thresholdChanged(value) { + this.changePlayerControl('threshold', value, true); + }, + brightnessRangeChanged(value) { + this.changePlayerControl('brightnessRange', value); + }, + hueRangeChanged(value) { + this.changePlayerControl('hueRange', value); + }, + playQueuePointerChanged(value) { + this.send('goToSong', value, false, true); + }, + clickSpeaker() { + this.simulateKick(1); + }, + dropFiles(files) { + this.setProperties({ + dragging: false, + draggingOverPlayListArea: false + }); + this.send('handleNewFiles', files); + }, + playerListAreaDragOver() { + this.set('draggingOverPlayListArea', true); + }, + playerListAreaDragLeave() { + this.set('draggingOverPlayListArea', false); + }, + handleNewFiles(files) { + let self = this, + playQueue = this.get('playQueue'), + updatePlayQueue = function () { + let tags = ID3.getAllTags("local"), + picture = null; + + if (tags.picture) { + let base64String = ""; + for (let i = 0; i < tags.picture.data.length; i++) { + base64String += String.fromCharCode(tags.picture.data[i]); + } + + picture = "data:" + tags.picture.format + ";base64," + window.btoa(base64String); + } + + playQueue.pushObject({ + fileName: this.name.replace(/\.[^/.]+$/, ""), + url: URL.createObjectURL(this), + artist: tags.artist, + title: tags.title, + picture: picture, + local: true + }); + + ID3.clearAll(); + + if (self.get('playQueuePointer') === -1) { + self.send('next'); + } + }; + + for (let key in files) { + if (files.hasOwnProperty(key)) { + let file = files[key]; + + if (file.type.startsWith('audio')) { + ID3.loadTags("local", updatePlayQueue.bind(file), { + dataReader: new FileAPIReader(file), + tags: ['title', 'artist', 'album', 'track', 'picture'] + }); + } + } + } + }, + toggleDimmer() { + this.sendAction('toggleDimmer'); + } + } +}); diff --git a/chrome/app/pods/components/music-tab/mixins/helpers.js b/chrome/app/pods/components/music-tab/mixins/helpers.js new file mode 100644 index 0000000..2d3d395 --- /dev/null +++ b/chrome/app/pods/components/music-tab/mixins/helpers.js @@ -0,0 +1,412 @@ +import Ember from 'ember'; + +const { + Mixin, + observer, + computed, + isNone, + run, + $, + inject, + on, + A +} = Ember; + +export default Mixin.create({ + classNames: ['col-sm-10', 'col-sm-offset-1', 'col-xs-12'], + classNameBindings: ['active::hidden'], + elementId: 'music-tab', + + dancer: null, + + notify: inject.service(), + + beatOptions: { + threshold: { + range: {min: 0, max: 0.5}, + step: 0.01, + defaultValue: 0.3, + pips: { + mode: 'values', + values: [0, 0.25, 0.5], + density: 10, + format: { + to: function ( value ) { + if(value === 0) { + value = 'More'; + } else if(value === 0.25) { + value = ''; + } else { + value = 'Less'; + } + + return value; + }, + from: function ( value ) { return value; } + } + } + }, + hueRange: { + range: {min: 0, max: 65535}, + step: 1, + defaultValue: 0.3, + pips: { + mode: 'values', + values: [0, 25500, 46920, 65535], + density: 10, + format: { + to: function ( value ) { + if(value === 0 || value === 65535) { + value = 'Red'; + } else if(value === 25500 ) { + value = 'Green'; + } else { + value = 'Blue'; + } + + return value; + }, + from: function ( value ) { return value; } + } + } + }, + brightnessRange: { + range: {min: 1, max: 254}, + step: 1, + defaultValue: 0, + pips: { + mode: 'values', + values: [1, 50, 100, 150, 200, 254], + density: 10, + format: { + to: function ( value ) { return value; }, + from: function ( value ) { return value; } + } + } + } + }, + + threshold: 0.3, + hueRange: [0, 65535], + brightnessRange: [1, 254], + oldThreshold: null, + + playQueuePointer: -1, + playQueue: A(), + timeElapsed: 0, + timeTotal: 0, + lastLightBopIndex: 0, + + playerBottomDisplayed: true, + dragging: false, + draggingOverPlayListArea: false, + dragLeaveTimeoutHandle: null, + audioStream: null, + dimmerOn: false, + isShowingAddSoundCloudModal: false, + + colorloopMode: false, + flashingTransitions: false, + + // 0 - no repeat, 1 - repeat all, 2 - repeat one + repeat: 0, + shuffle: false, + volumeMuted: false, + volume: 100, + // beat detection related pausing + paused: false, + // audio: playing or paused + playing: false, + songBeatPreferences: {}, + usingBeatPreferences: false, + oldBeatPrefCache: null, + storage: null, + firstVisit: true, + + soundCloudFuckUps: 0, + maxSoundCloudFuckUps: 3, + + // used to insure that we don't replay the same thing multiple times in shuffle mode + shufflePlayed: [], + + // noUiSlider connection specification + filledConnect: [true, false], + hueRangeConnect: [false, true, false], + + SC_CLIENT_ID: 'aeec0034f58ecd85c2bd1deaecc41594', + scUserNotSupportedHtml: '', + tooManySoundCloudFuckUps: '', + notStreamableHtml(fileNames){ + let html = ''; + + return html; + }, + urlNotFoundHtml(url){ + return ''; + }, + failedToPlayFileHtml(fileName){ + return ''; + }, + failedToDecodeFileHtml(fileName){ + return ''; + }, + + scUrl: computed('playQueuePointer', 'playQueue.[]', function(){ + let rtn = null, + currentSong = this.get('playQueue')[this.get('playQueuePointer')]; + + if(currentSong && currentSong.scUrl){ + rtn = currentSong.scUrl; + } + + return rtn; + }), + + playQueueEmpty: computed.empty('playQueue'), + playQueueNotEmpty: computed.notEmpty('playQueue'), + playQueueMultiple: computed('playQueue.[]', function(){ + return this.get('playQueue').length > 1; + }), + + seekPosition: computed('timeElapsed', 'timeTotal', function(){ + let timeTotal = this.get('timeTotal'), + timeElapsed = this.get('timeElapsed'); + + if (timeTotal === 0) { + return 0; + } + + return timeElapsed/timeTotal*100; + }), + + largeArtworkPic: computed('playQueuePointer', 'currentVisName', function(){ + let pic = '', + currentVisName = this.get('currentVisName'), + playQueuePointer = this.get('playQueuePointer'), + playQueue = this.get('playQueue'); + + if(playQueuePointer !== -1 && currentVisName === 'None'){ + let song = playQueue[playQueuePointer]; + if(!isNone(song.picture)){ + pic = song.picture; + + if(song.scUrl){ + pic = pic.replace('67x67', '500x500'); + } + } + } + + return pic; + }), + + repeatIcon: computed('repeat', function() { + if(this.get('repeat') === 2) { + return 'repeat-one'; + } + + return 'repeat'; + }), + + playingIcon: computed('playing', function() { + if(this.get('playing')){ + return 'pause'; + } else if(this.get('timeElapsed') === this.get('timeTotal') && this.get('timeTotal') !== 0){ + return 'replay'; + } else { + return 'play-arrow'; + } + }), + + playerAreaClickIcon: computed('playing', function() { + if(this.get('playing')){ + return 'play-arrow'; + } else { + return 'pause'; + } + }), + + playListAreaClass: computed('dragging', 'draggingOverPlayListArea', 'dimmerOn', function(){ + let classes = 'pointer'; + + if(this.get('dragging')){ + classes += ' drag-here-highlight'; + } + + if(this.get('draggingOverPlayListArea')){ + classes += ' dragging-over'; + } + + if(this.get('dimmerOn')){ + classes += ' dimmerOn'; + } + + return classes; + }), + + dimmerOnClass: computed('dimmerOn', function(){ + return this.get('dimmerOn') ? 'dimmerOn' : null; + }), + + volumeMutedClass: computed('volumeMuted', function(){ + let classes = 'player-control-icon volumeButton'; + + if(this.get('volumeMuted')){ + classes += ' active'; + } + + return classes; + }), + + repeatClass: computed('repeat', function(){ + return this.get('repeat') !== 0 ? 'player-control-icon active' : 'player-control-icon'; + }), + + shuffleClass: computed('shuffle', function(){ + return this.get('shuffle') ? 'player-control-icon active' : 'player-control-icon'; + }), + + volumeIcon: computed('volumeMuted', 'volume', function() { + let volume = this.get('volume'); + + if (this.get('volumeMuted')) { + return "volume-off"; + } else if (volume >= 70) { + return "volume-up"; + } else if (volume > 10) { + return "volume-down"; + } else { + return 'volume-mute'; + } + }), + + beatDetectionAreaArrowIcon: computed('playerBottomDisplayed', function(){ + if(!this.get('playerBottomDisplayed')){ + return 'keyboard-arrow-down'; + } else { + return 'keyboard-arrow-up'; + } + }), + + timeElapsedTxt: computed('timeElapsed', function(){ + return this.formatTime(this.get('timeElapsed')); + }), + + timeTotalTxt: computed('timeTotal', function() { + return this.formatTime(this.get('timeTotal')); + }), + + onPlayQueueChange: observer('playQueue.length', function(){ + let playQueueLength = this.get('playQueue.length'); + + if(playQueueLength > this.get('oldPlayQueueLength')){ + run.once(this, ()=>{ + run.next(this, function() { + $(`.track${playQueueLength-1}`).velocity('scroll', { container: $('#play-list-area'), duration: 200 }); + }); + }); + } + + this.set('oldPlayQueueLength', playQueueLength); + }), + + onColorloopModeChange: observer('colorloopMode', 'playing', function(){ + this.set('colorLoopOn', this.get('playing') && this.get('colorloopMode')); + }), + + onOptionChange: observer('flashingTransitions', 'playQueue.[]', 'playQueuePointer', 'colorloopMode', function(self, option){ + option = option.replace('.[]', ''); + let value = this.get(option); + + // can't really save local music + if(option === 'playQueue'){ + value = value.filter((song)=>{ + return !song.url.startsWith('blob:'); + }); + } + + this.get('storage').set('huegasm.' + option, value); + }), + + onRepeatChange: on('init', observer('repeat', function () { + let tooltipTxt = 'Repeat all', type = 'repeat'; + + if (this.get(type) === 1) { + tooltipTxt = 'Repeat one'; + } else if (this.get(type) === 2) { + tooltipTxt = 'Repeat off'; + } + + this.changeTooltipText(type, tooltipTxt); + })), + + onShuffleChange: on('init', observer('shuffle', function () { + let tooltipTxt = 'Shuffle', type = 'shuffle'; + + if (this.get(type)) { + this.get('shufflePlayed').clear(); + tooltipTxt = 'Unshuffle'; + } + + this.changeTooltipText(type, tooltipTxt); + })), + + onVolumeMutedChange: on('init', observer('volumeMuted', function() { + let tooltipTxt = 'Mute', type = 'volumeMuted', + volumeMuted = this.get(type), dancer = this.get('dancer'), + volume=0; + + if (volumeMuted) { + tooltipTxt = 'Unmute'; + volume = 0; + } else { + volume = this.get('volume')/100; + } + + if(this.get('playing')){ + dancer.setVolume(volume); + } + + this.changeTooltipText(type, tooltipTxt); + })), + + onPrevChange: on('init', observer('timeElapsed', 'playQueueNotEmpty', 'playQueue.[]', function() { + if(this.get('playQueueNotEmpty')){ + let tooltipTxt = 'Previous', type = 'prev'; + + if(this.get('timeElapsed') > 5 || this.get('playQueue').length === 1) { + tooltipTxt = 'Replay'; + } + + this.changeTooltipText(type, tooltipTxt); + } + })), + + onPlayingChange: on('init', observer('playing', function () { + let tooltipTxt = 'Play', type = 'playing'; + + if (this.get(type)) { + tooltipTxt = 'Pause'; + } else if(this.get('timeElapsed') === this.get('timeTotal') && this.get('timeTotal') !== 0){ + tooltipTxt = 'Replay'; + } + + this.changeTooltipText(type, tooltipTxt); + })), + + changeTooltipText(type, text) { + // change the tooltip text if it's already visible + $('#' + type + 'Tooltip + .tooltip .tooltip-inner').html(text); + //change the tooltip text for hover + $('#' + type + 'Tooltip').attr('data-original-title', text); + + if(isNone(this.get(type + 'TooltipTxt'))) { + this.set(type + 'TooltipTxt', text); + } + }, + + formatTime(time){ + return this.pad(Math.floor(time/60), 2) + ':' + this.pad(time%60, 2); + }, + + pad(num, size){ return ('000000000' + num).substr(-size); } +}); diff --git a/chrome/app/pods/components/music-tab/mixins/visualizer.js b/chrome/app/pods/components/music-tab/mixins/visualizer.js new file mode 100644 index 0000000..17dcfe2 --- /dev/null +++ b/chrome/app/pods/components/music-tab/mixins/visualizer.js @@ -0,0 +1,94 @@ +import Ember from 'ember'; + +const { + Mixin, + observer, + $ +} = Ember; + +export default Mixin.create({ + currentVisName: 'None', + + visNames: ['None', 'Bars', 'Wave'], + + onCurrentVisNameChange: observer('currentVisName', function () { + let currentVisName = this.get('currentVisName'); + + if(currentVisName === 'None'){ + let canvasEl = $('#visualization')[0], + ctx = canvasEl.getContext('2d'); + + ctx.clearRect(0, 0, canvasEl.width, canvasEl.height); + } + + this.get('storage').set('huegasm.currentVisName', currentVisName); + }), + + didInsertElement(){ + let dancer = this.get('dancer'), + canvas = $('#visualization')[0], + playerArea = $('#player-area'), + ctx = canvas.getContext('2d'), + spacing = 2, + h = playerArea.height(), w; + + canvas.height = h; + + // must be done to preserver resolution so that things don't appear blurry + // note that the height is set to 400px via css so it doesn't need to be recalculated + let syncCanvasHeight = ()=>{ + w = playerArea.width(); + canvas.width = w; + }; + + syncCanvasHeight(); + + $(window).on('resize', syncCanvasHeight); + + dancer.bind('update', () => { + let currentVisName = this.get('currentVisName'), + gradient = ctx.createLinearGradient(0, 0, 0, h), + pageHidden = document.hidden || document.msHidden || document.webkitHidden || document.mozHidden; + + // dont do anything if the page is hidden or no visualization + if(currentVisName === 'None' || pageHidden || !this.get('active')){ + return; + } + + ctx.clearRect(0, 0, w, h); + + if (currentVisName === 'Wave') { + let width = 3, + count = 1024; + + gradient.addColorStop(0.6, 'white'); + gradient.addColorStop(0, '#0036FA'); + + ctx.lineWidth = 1; + ctx.strokeStyle = gradient; + let waveform = dancer.getWaveform(); + + ctx.beginPath(); + ctx.moveTo(0, h / 2); + for (let i = 0, l = waveform.length; i < l && i < count; i++) { + ctx.lineTo(i * ( spacing + width ), ( h / 2 ) + waveform[i] * ( h / 2 )); + } + ctx.stroke(); + ctx.closePath(); + } else if (currentVisName === 'Bars') { + let width = 4, + count = 128; + + gradient.addColorStop(1, '#0f0'); + gradient.addColorStop(0.6, '#ff0'); + gradient.addColorStop(0.2, '#F12B24'); + + ctx.fillStyle = gradient; + let spectrum = dancer.getSpectrum(); + for (let i = 0, l = spectrum.length; i < l && i < count; i++) { + ctx.fillRect(i * ( spacing + width ), h, width, -spectrum[i] * h - 60); + } + } + }); + } +}); diff --git a/chrome/app/pods/components/music-tab/template.hbs b/chrome/app/pods/components/music-tab/template.hbs new file mode 100644 index 0000000..8ed2441 --- /dev/null +++ b/chrome/app/pods/components/music-tab/template.hbs @@ -0,0 +1,194 @@ +
+
+ + +
+ +
+ + {{paper-icon playerAreaClickIcon id="play-notification"}} + +
+ {{range-slider start=seekPosition min=0 max=100 connect=filledConnect id="seek-slider" on-change="seekChanged"}} + + {{#if playQueueNotEmpty}} + {{paper-icon "skip-previous" class="player-control-icon"}}{{/if}}{{paper-icon playingIcon class="player-control-icon"}}{{#if playQueueMultiple}}{{paper-icon "skip-next" action="" class="player-control-icon"}}{{/if}}{{paper-icon icon=volumeIcon class=volumeMutedClass}}{{range-slider start=volume min=0 max=100 connect=filledConnect on-change="volumeChanged" id="volume-bar" class="hidden-xs"}} + +
{{timeElapsedTxt}} / {{timeTotalTxt}}
+ + {{#paper-menu as |menu|}} + {{#menu.trigger}} + {{#paper-button iconButton=true}} + {{paper-icon "remove-red-eye" class="player-control-icon"}} + {{/paper-button}} + {{/menu.trigger}} + {{#menu.content width=2 as |content|}} + {{#each visNames as |name|}} + {{#content.menu-item onClick=(action "setVisName" name)}} + {{name}} + + {{#if (eq currentVisName name)}} + {{paper-icon "check" classNames=dimmerOnClass}} + {{/if}} + {{/content.menu-item}} + {{/each}} + {{/menu.content}} + {{/paper-menu}} + + {{#if scUrl}} + + + + + {{/if}} +
+
+ +
+ + +
+ {{#paper-menu as |menu|}} + {{#menu.trigger}} + {{#paper-button iconButton=false}} + {{paper-icon "playlist add" class="player-control-icon"}} Add new music + {{/paper-button}} + {{/menu.trigger}} + {{#menu.content width=3 as |content|}} + {{#content.menu-item onClick="addLocalAudio"}} + {{paper-icon "attachment" class=shuffleClass}} Local file + {{/content.menu-item}} + {{#content.menu-item onClick="toggleIsShowingAddSoundCloudModal"}} + {{paper-icon "cloud" class=shuffleClass}} SoundCloud + {{/content.menu-item}} + {{/menu.content}} + {{/paper-menu}} + + {{paper-icon "shuffle" class=shuffleClass}} + {{paper-icon repeatIcon class=repeatClass}} + +
+ +
+ {{#if (or playQueueEmpty dragging)}} +
+ {{#if dragging}} + Drag your music files here + {{else}} + Add your music files here + {{/if}} +
+ {{paper-icon "library-music" class=dimmerOnClass}} + {{/if}} + + {{#each playQueue as |item index|}} +
+ {{#if item.picture}} + + {{else}} + + {{/if}} + +
+ {{#if item.title}} +
{{item.title}}
+
+ {{#if item.artistUrl}} + {{item.artist}} + {{else}} + {{item.artist}} + {{/if}} +
+ {{else}} + {{item.fileName}} + {{/if}} +
+ + {{paper-icon "close" classNames="close"}} +
+ {{/each}} +
+
+
+ +
+
+ {{paper-icon beatDetectionAreaArrowIcon id="beat-detection-area-arrow-icon"}} +
+
+ +
+
+ {{#if usingBeatPreferences}} + + {{paper-icon "star" class=dimmerOnClass}} + + {{/if}} + +
+
+ + Hue Range + + + {{range-slider start=hueRange orientation="vertical" step=beatOptions.hueRange.step range=beatOptions.hueRange.range connect=hueRangeConnect on-change="hueRangeChanged" pips=beatOptions.hueRange.pips}} +
+ +
+ + Brightness Range + + + {{range-slider start=brightnessRange orientation="vertical" step=beatOptions.brightnessRange.step range=beatOptions.brightnessRange.range on-change="brightnessRangeChanged" pips=beatOptions.brightnessRange.pips}} +
+ +
+ + Sensitivity + + + {{range-slider start=threshold orientation="vertical" step=beatOptions.threshold.step range=beatOptions.threshold.range on-change="thresholdChanged" pips=beatOptions.threshold.pips}} +
+ +
+ + {{paper-checkbox value=flashingTransitions onChange=(action (mut flashingTransitions)) label="Flashing Transitions"}} + + + + {{paper-checkbox value=colorloopMode onChange=(action (mut colorloopMode)) label="Colorloop"}} + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+ +{{music-tab/add-soundcloud-sound-modal action="handleNewSoundCloudURL" isShowingModal=isShowingAddSoundCloudModal}} \ No newline at end of file diff --git a/chrome/app/resolver.js b/chrome/app/resolver.js new file mode 100644 index 0000000..2fb563d --- /dev/null +++ b/chrome/app/resolver.js @@ -0,0 +1,3 @@ +import Resolver from 'ember-resolver'; + +export default Resolver; diff --git a/chrome/app/router.js b/chrome/app/router.js new file mode 100644 index 0000000..cdc2578 --- /dev/null +++ b/chrome/app/router.js @@ -0,0 +1,12 @@ +import Ember from 'ember'; +import config from './config/environment'; + +const Router = Ember.Router.extend({ + location: config.locationType, + rootURL: config.rootURL +}); + +Router.map(function() { +}); + +export default Router; diff --git a/chrome/app/styles/app.scss b/chrome/app/styles/app.scss new file mode 100644 index 0000000..c0b4812 --- /dev/null +++ b/chrome/app/styles/app.scss @@ -0,0 +1,118 @@ +@import 'ember-modal-dialog/ember-modal-structure'; +@import 'ember-modal-dialog/ember-modal-appearance'; + +@import 'huegasm-variables'; + +@import 'bootstrap'; // used to take out bootstrap scss modules that we don't need +@import 'paper'; + +@import 'bridge-finder'; +@import 'common'; +@import 'dimmer'; +@import 'fancy-speaker'; +@import 'introjs'; +@import 'hue-controls'; +@import 'light-group'; +@import 'music-tab'; +@import 'noui-slider'; + +body { + min-width: 500px; +} + +body > .ember-view { + display: flex; + min-height: 100vh; + flex-direction: column; +} + +body, button { + font-family: 'Slabo 27px', serif; +} + +#huegasm { + flex: 1; +} + +.ember-app { + padding-bottom: 50px; +} + +.footer { + margin: 0 auto 10px auto; + width: 100%; + max-width: 800px; + text-align: center; + display: flex; + align-items: center; + justify-content: space-around; + +} + +.footer-text { + display: inline-block; + font-size: 18px; + a { + margin-left: 5px; + } +} + +.alert { + margin-bottom: 0; + border: none; +} + + +.title { + margin-bottom: 20px; + img { + width: 200px; + } +} + +button.md-warn { + background: $secondaryThemeColor; +} + +div.ember-modal-dialog { + padding: 20px; + color: $blackish; + md-input-container { + width: 100%; + input.md-input[type="text"] { + color: $blackish !important; + } + } + md-input-container label { + color: rgba(0, 0, 0, 0.26); + } +} + +.display-flex { + display: flex !important; +} + +// fancy webkit scrollbars +::-webkit-scrollbar { + -webkit-appearance: none; +} + +::-webkit-scrollbar:vertical { + width: 12px; +} + +::-webkit-scrollbar:horizontal { + height: 12px; +} + +::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, .5); + border-radius: 10px; + border: 2px solid #ffffff; + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5); +} + +::-webkit-scrollbar-track { + background-color: #ffffff; +} + diff --git a/chrome/app/styles/bootstrap.scss b/chrome/app/styles/bootstrap.scss new file mode 100644 index 0000000..f5e4339 --- /dev/null +++ b/chrome/app/styles/bootstrap.scss @@ -0,0 +1,56 @@ +/*! + * Bootstrap v3.3.7 (http://getbootstrap.com) + * Copyright 2011-2016 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +// Core variables and mixins +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/variables"; +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/mixins"; + +// Reset and dependencies +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/normalize"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/print"; +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/glyphicons"; + +// Core CSS +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/scaffolding"; +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/type"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/code"; +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/grid"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/tables"; +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/forms"; +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/buttons"; + +// Components +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/component-animations"; +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/dropdowns"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/button-groups"; +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/input-groups"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/navs"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/navbar"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/breadcrumbs"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/pagination"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/pager"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/labels"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/badges"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/jumbotron"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/thumbnails"; +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/alerts"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/progress-bars"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/media"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/list-group"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/panels"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/responsive-embed"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/wells"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/close"; + +// Components w/ JavaScript +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/modals"; +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/tooltip"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/popovers"; +//@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/carousel"; + +// Utility classes +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/utilities"; +@import "bower_components/bootstrap-sass/assets/stylesheets/bootstrap/responsive-utilities"; diff --git a/chrome/app/styles/bridge-finder.scss b/chrome/app/styles/bridge-finder.scss new file mode 100644 index 0000000..f6ef407 --- /dev/null +++ b/chrome/app/styles/bridge-finder.scss @@ -0,0 +1,73 @@ +#press-bridge-button-img { + width: 200px; + margin: 0 auto 30px auto; + display: inherit; +} + +#bridge-button-group { + width: 150px; + margin: 30px auto; + text-align: left; +} + +#bridge-input md-input-container{ + max-width: 200px; + margin: 30px auto 20px; +} + +#intro { + font-size: 22px; +} + +#intro-paragraph { + margin-bottom: 20px; + font-size: 16px; +} + +#bridge-finder, .ready-block { + text-align: center; + padding: 10px 15px 0; + font-size: 16px; +} + +#bridge-finder { + min-height: 500px; + .md-bar { + background-color: $secondaryThemeColor !important; + } +} + +// preloading image +.ready-block:after { + display: none; + content: url(images/pressButtonBridge.png); +} + +.embed-container { + position:relative; + padding-bottom:56.25%; + padding-top:30px; + height:0; + overflow:hidden; +} + +.embed-container-wrapper { + max-width: 550px; + margin: auto; +} + +.embed-container iframe, .embed-container object, .embed-container embed { + position:absolute; + top:0; + left:0; + width:100%; + height:100%; +} + +.go-button { + margin: 20px 0; + border-radius: 100% !important; + width: 100px; + height: 100px; + font-size: 28px; +} diff --git a/chrome/app/styles/common.scss b/chrome/app/styles/common.scss new file mode 100644 index 0000000..db7ef0f --- /dev/null +++ b/chrome/app/styles/common.scss @@ -0,0 +1,15 @@ +.text-left { + text-align: left !important; +} + +.relative { + position: relative !important; +} + +.no-text-decoration { + text-decoration: none !important; +} + +.pointer { + cursor: pointer; +} \ No newline at end of file diff --git a/chrome/app/styles/dimmer.scss b/chrome/app/styles/dimmer.scss new file mode 100644 index 0000000..b36f0d3 --- /dev/null +++ b/chrome/app/styles/dimmer.scss @@ -0,0 +1,76 @@ +div.dimmerOn { + color: $whitish !important; + background: $blackish !important; +} + +html.dimmerOn { + color: white; + background: $blackish; +} + +body.dimmerOn { + color: $whitish; + background: $blackish; + md-input-container { + label { + color: #3f51b5 !important; + } + .md-input { + color: $whitish !important; + border-color: #3f51b5 !important; + } + } + .md-track { + background: $whitish; + } + .color { + border: 1px solid white; + } + .playlist-item, .ember-basic-dropdown-content md-menu-content { + color: $whitish; + background-color: $dimmerOnButtonColor; + } + .ember-basic-dropdown-content a { + color: $whitish; + } + .playlist-item { + &.active { + background: darken($dimmerOnButtonColor, 15%) !important; + } + &:hover { + background: darken($dimmerOnButtonColor, 10%); + } + .audio-remove-button .paper-icon { + color: $whitish !important; + &:hover { + color: white !important; + } + } + } + svg { + -webkit-filter: drop-shadow(0 0 5px #228DFF); + } + .md-container { + color: $whitish; + } + .add-new-music:hover { + background: darken($dimmerOnButtonColor, 5%); + } + .md-bar { + background-color: darken(white, 60%) !important; + } +} + +.paper-icon.dimmerOn { + text-shadow: $glowingText; + opacity: 0.9 !important; +} + +.logo { + display: inline-block; + cursor: pointer; + width: 40px; + height: 40px; + background: url(images/huegasm.png) center center no-repeat; + background-size: 40px 40px; +} \ No newline at end of file diff --git a/chrome/app/styles/fancy-speaker.scss b/chrome/app/styles/fancy-speaker.scss new file mode 100644 index 0000000..b9e1ac7 --- /dev/null +++ b/chrome/app/styles/fancy-speaker.scss @@ -0,0 +1,101 @@ +$centersize: 80px; +$center1size: 205px; +$bezelsize: 240px; + +%base { + border-radius: 100%; +} +%rivet { + position: absolute; + height: 8px; + width: 8px; + background-color: #555; + border-radius: 100%; + box-shadow: inset 0 0 3px #000, 0 0 2px #000; +} + +#beat-speaker-center-inner { + @extend %base; + height: $centersize; + width: $centersize; + position: absolute; + bottom: 47px; + right: 47px; + -webkit-filter: blur(1px); + filter: blur(1px); + background: rgb(0,0,0); + background: -moz-radial-gradient(center, ellipse cover, rgba(0,0,0,1) 0%, rgba(79,79,79,1) 0%, rgba(0,0,0,1) 100%); + background: -webkit-gradient(radial, center center, 0px, center center, 100%, color-stop(0%,rgba(0,0,0,1)), color-stop(0%,rgba(79,79,79,1)), color-stop(100%,rgba(0,0,0,1))); + background: -webkit-radial-gradient(center, ellipse cover, rgba(0,0,0,1) 0%,rgba(79,79,79,1) 0%,rgba(0,0,0,1) 100%); + background: -o-radial-gradient(center, ellipse cover, rgba(0,0,0,1) 0%,rgba(79,79,79,1) 0%,rgba(0,0,0,1) 100%); + background: -ms-radial-gradient(center, ellipse cover, rgba(0,0,0,1) 0%,rgba(79,79,79,1) 0%,rgba(0,0,0,1) 100%); + background: radial-gradient(ellipse at center, rgba(0,0,0,1) 0%,rgba(79,79,79,1) 0%,rgba(0,0,0,1) 100%); + box-shadow: 0 0 10px rgba(0, 0, 0, 1); +} + +#beat-speaker-center-outer { + @extend %base; + position: absolute; + top: 16px; + left: 16px; + height: $center1size; + width: $center1size; + border: 15px solid #333; + box-shadow: -3px -3px 15px rgba(0, 0, 0, 0.4), inset -3px -3px 15px rgba(0, 0, 0, 0.5); + background: -moz-linear-gradient(130deg, rgba(117, 117, 117, 1) 55%, rgba(220, 220, 220, 1) 100%); + background: -webkit-linear-gradient(130deg, rgba(117, 117, 117, 1) 55%, rgba(220, 220, 220, 1) 100%); + background: -o-linear-gradient(130deg, rgba(117, 117, 117, 1) 55%, rgba(220, 220, 220, 1) 100%); + background: -ms-linear-gradient(130deg, rgba(117, 117, 117, 1) 55%, rgba(220, 220, 220, 1) 100%); + background: linear-gradient(130deg, rgba(117, 117, 117, 1) 55%, rgba(220, 220, 220, 1) 100%); +} + +.bezel { + @extend %base; + margin: 0 auto; + height: $bezelsize; + width: $bezelsize; + position: relative; + background-color: #A8A8A8; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.8), inset 3px 3px 10px rgba(0, 0, 0, 0.8), 0 0 2px rgba(0, 0, 0, 0.8), inset 0 0 30px -5px rgba(0, 0, 0, 0.8); +} + +.rivet1 { + @extend %rivet; + top: 6px; + left: 50%; +} +.rivet2 { + @extend %rivet; + bottom: 6px; + left: 50%; +} +.rivet3 { + @extend %rivet; + top: 50%; + left: 6px; +} +.rivet4 { + @extend %rivet; + top: 50%; + right: 6px; +} +.rivet5 { + @extend %rivet; + top: 18%; + left: 13.7%; +} +.rivet6 { + @extend %rivet; + top: 18%; + right: 13.5%; +} +.rivet7 { + @extend %rivet; + bottom: 17%; + left: 13.5%; +} +.rivet8 { + @extend %rivet; + bottom: 17%; + right: 13.5%; +} diff --git a/chrome/app/styles/hue-controls.scss b/chrome/app/styles/hue-controls.scss new file mode 100644 index 0000000..e517619 --- /dev/null +++ b/chrome/app/styles/hue-controls.scss @@ -0,0 +1,112 @@ +#lights-tab { + padding: 0; + margin-top: 5vh; + .paper-icon { + line-height: 0.8 !important; + } +} + +.lights-control-tooltip + .tooltip { + left: 0 !important; +} + +#color-row { + cursor: pointer; + .md-list-item-inner { + padding-right: 0; + } +} + +#hue-controls { + max-width: 1200px; + height: 90vh; + md-progress-circular { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +} + +// preload images +#hue-controls:after, md-progress-circular:after { + display: none; + content: url(images/colormap.png) url(images/missingArtwork.png) url(images/sc-white.png) url(images/huegasm.png) url(images/lights/a19.svg) url(images/lights/a19w.svg) url(images/lights/br30.svg) url(images/lights/br30w.svg) url(images/lights/gu10.svg) url(images/lights/gu10w.svg) url(images/lights/huego.svg) url(images/lights/huegow.svg) url(images/lights/lc_aura.svg) url(images/lights/lc_auraw.svg) url(images/lights/lc_bloom.svg) url(images/lights/lc_bloomw.svg) url(images/lights/lc_iris.svg) url(images/lights/lc_irisw.svg) url(images/lights/lightstrip.svg) url(images/lights/lightstripw.svg) url(images/lights/storylight.svg) url(images/lights/storylightw.svg); +} + +#navigation { + padding: 15px 0 4vh; + text-align: center; + margin: auto; + position: relative; + .ember-basic-dropdown-trigger { + z-index: 3; + text-align: right; + position: absolute; + top: -10px; + right: 10px; + transform: scale(1.1); + } +} + +.navigation-item { + font-size: 18px; + padding: 0 10px 0 10px; + &.active { + font-weight: bold; + cursor: default; + text-decoration: none !important; + } + &:hover { + text-decoration: underline; + } +} + +.color { + border: 1px solid rgba(0, 0, 0, 0.5); +} + +#color-picker { + padding: 5px; + background: rgba(0, 0, 0, 0.7); + box-shadow: 5px 10px 15px 5px rgba(0, 0, 0, 0.3); + color: #FFFFFF; + position: absolute; + width: 266px; + height: 266px; + right: 6px; + top: -9px; + z-index: 3; +} + +.color-content { + box-shadow: none !important; + md-menu-content, md-menu-item { + background-color: transparent !important; + } +} + +#picker { + cursor: crosshair; +} + +#loop-addition { + position: absolute; + left: 33px; + top: 15px; + font-size: 16px !important; +} + +#huegasm-content { + height: 80%; + max-height: 500px; +} + +@media(min-width:767px) { + #lights-tab { + font-size: 20px; + .paper-icon { + font-size: 24px; + } + } +} diff --git a/chrome/app/styles/huegasm-variables.scss b/chrome/app/styles/huegasm-variables.scss new file mode 100644 index 0000000..914abb3 --- /dev/null +++ b/chrome/app/styles/huegasm-variables.scss @@ -0,0 +1,8 @@ +$playerHeight: 400px; +$playerDefaultIconColor: #BBBBBB; +$secondaryThemeColor: #F12B24; +$glowingText: 0 0 2px #fff, 0 0 4px #fff, 0 0 20px #228DFF; +$dimmerOnButtonColor: #404040; +$blackish: #242424; +$whitish: #e0e0e0; +$paperThemeColor: #3f51b5; diff --git a/chrome/app/styles/introjs.scss b/chrome/app/styles/introjs.scss new file mode 100644 index 0000000..fc7b5a1 --- /dev/null +++ b/chrome/app/styles/introjs.scss @@ -0,0 +1,18 @@ +#settings.introjs-fixParent { + position: inherit !important; +} + +.introjs-tooltip { + width: 300px; +} + +.introjs-skipbutton { + color: $secondaryThemeColor; +} + +.introjs-bullets ul li a.active { + position: relative; + height: 10px; + width: 10px; + top: -2px; +} diff --git a/chrome/app/styles/light-group.scss b/chrome/app/styles/light-group.scss new file mode 100644 index 0000000..c8e56f8 --- /dev/null +++ b/chrome/app/styles/light-group.scss @@ -0,0 +1,71 @@ +.light-group { + max-width: 800px; + margin: 0 auto; + display: flex; + justify-content: center; + .tooltip.top { + margin-top: 1px; + margin-left: 2px; + } +} + +.toggleable-light { + cursor: pointer; + position: relative; + border-radius: 30%; + border: 2px solid $whitish; + margin: 0 2px; + display: flex; + height: 50px; + align-items: center; + justify-content: center; +} + +.light-inactive { + border-color: rgba($secondaryThemeColor, 0.4); +} + +.light-inactive::before { + font-weight: bold; + position: absolute; + top: -5px; + content: "\e014"; + font-family: 'Glyphicons Halflings'; + font-size: 40px; + color: rgba($secondaryThemeColor, 0.6); +} + +.light-active { + border-color: rgba(green, 0.4); + img { + transition-duration: 0.3s; + transition-property: transform; + box-shadow: 0 0 1px rgba(0, 0, 0, 0); + } + img:hover { + transform: scale(1.2); + } +} + +.ember-modal-overlay.translucent { + background-color: rgba(0, 0, 0, 0.50); +} + +.remove-button { + margin: 10px 0 10px 60px; +} + +.light-text { + width: 60px; + word-wrap: break-word; + padding: 0 10px; +} + +.light-text-content { + display: block; /* Fallback for non-webkit */ + display: -webkit-box; + overflow: hidden; + text-overflow: ellipsis; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; +} \ No newline at end of file diff --git a/chrome/app/styles/music-tab.scss b/chrome/app/styles/music-tab.scss new file mode 100644 index 0000000..21b9fc4 --- /dev/null +++ b/chrome/app/styles/music-tab.scss @@ -0,0 +1,496 @@ +.row { + margin: 0; +} + +#music-tab { + padding: 0; + margin-top: 10px; + margin-bottom: 20px; +} + +#slide-toggle { + font-size: 22px; + color: $playerDefaultIconColor; + background: #730B07; + div .paper-icon { + color: inherit !important; + font-size: 24px; + font-weight: bold; + } +} + +#slide-toggle:hover { + color: lighten($playerDefaultIconColor, 30%) !important; +} + +#player-controls { + transition: all 0.2s ease-in-out; + position: absolute; + bottom: 0; + left: 0; + padding: 15px 10px; + width: 100%; + color: white !important; + z-index: 20; + cursor: default; + background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 1)); + .ember-basic-dropdown-trigger { + position: absolute; + right: 0; + bottom: 13px; + } + .tooltip.top { + margin-top: -17px; + } + .tooltip-arrow { + display: none; + } + md-menu-item>.md-button md-icon { + margin: auto 0 5px 10px; + } + .play-arrow, + .pause, + .replay { + font-size: 30px; + } +} + +#player-time-controls { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + display: inline-block; + margin-left: 1em; +} + +.player-control-icon { + color: $playerDefaultIconColor !important; + transition-duration: 0.1s; + margin-right: 10px; + margin-top: 4px; + font-size: 22px; +} + +.player-control-icon.active { + color: $secondaryThemeColor !important; +} + +.player-control-icon:hover { + color: white !important; +} + +#play-notification { + position: relative; + color: white !important; + background: black; + top: 50%; + left: 50%; + opacity: 0; + border-radius: 100%; +} + +#player-area { + height: $playerHeight; + background-color: black; + display: inline-block; + padding: 0; + cursor: pointer; +} + +#playlist { + height: $playerHeight; + background-color: #1E1E1E; + padding: 0 5px 0 5px; +} + +#player-area * .noUi-origin { + background-color: $blackish; + border-radius: 5px; +} + +#player-area * .noUi-base { + background-color: $blackish; + border-radius: 5px; +} + +#volume-bar { + width: 5em; + height: 0.5em; + display: inline-block; +} + +#player-area * .noUi-handle::after, +#player-area * .noUi-handle::before { + content: none; +} + +#seek-slider { + margin-bottom: 15px; + transition-duration: 0.2s; + height: 8px; + .noUi-handle { + opacity: 1 !important; + } +} + +#seek-slider:hover { + height: 8px; +} + +#seek-slider:hover * .noUi-handle { + opacity: 1; +} + +#seek-slider * .noUi-handle { + border: none; + height: 13px; + width: 13px; + border-radius: 50%; + top: -4px; + left: -6px; + opacity: 0; + transition-duration: 0.1s; + background-color: $secondaryThemeColor !important; + box-shadow: none; +} + +#play-list-controls { + min-height: 40px; + margin-top: 5px; + border-bottom: 1px solid #3a3a3a; + position: relative; + button .player-control-icon { + margin: 0 5px 1px 3px; + } + .ember-basic-dropdown-trigger { + position: absolute; + bottom: 0; + right: 0; + color: $whitish; + .paper-button { + margin: 0; + } + } +} + +#play-list-area { + background-color: white; + width: 100%; + height: 350px; + margin: 0 auto; + border-radius: 5px; + transition: 0.1s all ease-in-out; + position: relative; + overflow: auto; + #dragHere { + position: absolute; + top: 27%; + font-size: 20px; + text-align: center; + width: 100%; + } + [md-font-icon="library-music"] { + position: absolute; + top: 40%; + font-size: 100px; + opacity: 0.5; + width: 100%; + text-align: center; + } +} + +.song-artist { + font-weight: bold; +} + +#play-list-area.drag-here-highlight { + background-color: white; + border: 5px dotted #5383ff; +} + +#play-list-area.dragging-over { + background-color: darken(white, 5%); + box-shadow: inset 0 0 20px 0 rgba(0, 0, 0, 1); +} + +#file-input { + width: 1px; + height: 1px; + visibility: hidden; +} + +.playlist-item { + border-bottom: 1px solid rgba(128, 128, 128, 0.3); + border-top: 1px solid rgba(128, 128, 128, 0.3); + height: 62px; + font-family: 'Open Sans', sans-serif; + padding: 0 20px 0 5px; + position: relative; + color: $blackish; + background: darken(white, 5%); + .close { + font-size: 18px; + } + .album-art { + height: 60px; + float: left; + margin-right: 5px; + border: 1px solid rgba(0, 0, 0, 0.5); + } + .song-info { + .song-title { + max-height: 40px; + overflow: hidden; + } + .song-artist { + max-height: 20px; + overflow: hidden; + } + } + .audio-remove-button { + position: absolute; + top: 10px; + right: 0; + padding: 10px; + } +} + +.playlist-item.active { + background: darken(white, 15%) !important; + border-top: 1px solid $secondaryThemeColor; + border-bottom: 1px solid $secondaryThemeColor; +} + +.playlist-item:hover { + background: darken(white, 10%); + .close { + display: block; + } +} + +#beat-area { + position: relative; + padding: 0; +} + +.star { + cursor: auto !important; +} + +#beat-option-button-group { + margin: 20px 0 10px 0; +} + +#light-option { + margin-top: 20px; + display: flex; + justify-content: space-around; + .md-label { + width: auto; + } +} + +.beat-option { + padding: 5px 0; + text-align: center; + md-checkbox { + padding: 10px 0; + } + md-switch { + margin: 0; + } + .option-description { + display: inline-flex; + font-size: 20px; + justify-content: center; + flex-direction: column; + } + button { + margin-top: 0; + } + .tooltip { + margin: 0; + } +} + +#player-bottom { + color: $blackish; + border: 1px solid black; + width: 100%; + background: white; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + display: flex; + align-items: center; +} + +#beat-container { + display: flex; + margin-bottom: 10px; +} + +#beat-area .light-group { + margin: 10px 20px 0 40px; + float: right; + div { + display: block; + padding: 10px; + } +} + +#add-music-choices { + min-width: initial; + right: 0; + left: initial; + width: 100px; + top: 25px; +} + +.add-new-music { + padding: 0 5px 0 10px; + font-size: 16px; + border-radius: 5px; + background: #f8f8f8; + border: none; +} + +.add-new-music:hover { + background: darken(#f8f8f8, 5%); +} + +.sound-cloud-link { + position: absolute; + right: 55px; + bottom: 22px; +} + +#visualization { + position: absolute; + top: 0; + left: 0; +} + +#save-beat-preferences-star { + position: absolute; + top: 5px; + left: 5px; + z-index: 1000; + md-icon { + color: $secondaryThemeColor !important; + font-size: 25px; + cursor: default; + } +} + +.visualizers-menu { + left: -135px; +} + +.display-icon { + background: url(images/huegasm.png) center center no-repeat; + background-size: 80px 80px; +} + +#artwork { + position: absolute; + width: 100%; + overflow: hidden; + img { + display: block; + margin: 0 auto; + max-height: 400px; + } +} + +.keyboard-arrow-down { + font-size: 20px; +} + +.visualizers-menu .paper-icon { + margin-left: 10px; + position: relative; + top: -4px; +} + +.close { + font-size: 18px !important; + color: rgb(51, 51, 51); + display: none; + text-shadow: none; + &:hover { + color: darken(#333333, 5%) !important; + } +} + +.ember-notify-default.ember-notify-cn { + top: 0; + bottom: initial; +} + +#soundcloud-logo { + display: block; +} + +#soundcloud-logo-small { + display: none; +} + +#soundcloud-tutorial { + width: 100%; +} + +@media(max-width:1100px) { + #soundcloud-logo { + display: none; + } + #soundcloud-logo-small { + display: block; + } +} + +@media(min-width:767px) and (max-width:1200px) { + #add-new-music-label { + display: none; + } + #play-list-controls .paper-button { + border: 1px solid $whitish; + border-radius: 5px; + } +} + +@media(max-width: 500px) { + #sensitivity-settings .noUi-value-vertical { + display: none; + } + .option-description { + height: 55px; + } +} + +// mobile overrides +@media(max-width:767px) { + div#player-bottom { + display: block !important; + } + #beat-area { + height: initial; + } + #seek-slider { + height: 8px; + .noUi-handle { + opacity: 1 !important; + } + } + #seek-slider { + margin-bottom: 15px; + } + .close { + display: block; + } + #save-beat-preferences-star { + right: 5px; + left: initial; + } + md-checkbox { + padding-right: 20px !important; + } +} diff --git a/chrome/app/styles/noui-slider.scss b/chrome/app/styles/noui-slider.scss new file mode 100644 index 0000000..06c756b --- /dev/null +++ b/chrome/app/styles/noui-slider.scss @@ -0,0 +1,56 @@ +.noUi-value-vertical { + margin-top: -10px; + transform: none; +} + +.noUi-value-vertical, .noUi-pips { + color: inherit !important; +} + +.noUi-vertical .noUi-handle { + border: 1px solid #A3A0A0; + width: 26px; +} + +.noUi-vertical .noUi-handle:after, .noUi-vertical .noUi-handle:before{ + background: grey; +} + +.noUi-base { + cursor: pointer; +} + +.noUi-connect { + background-color: $secondaryThemeColor; +} + +.noUi-handle { + cursor: pointer; +} + +.noUi-horizontal .noUi-handle { + width: 0.4em; + height: 1.3em; + left: -0.071em; + top: -0.550em; + transition-duration: 0.1s; + background: $playerDefaultIconColor !important; +} + +.noUi-horizontal .noUi-handle:hover { + background: white !important; +} + +.noUi-target { + margin: 0 auto; +} + +.noUi-base { + background-color: #ADADAD; + border: 1px solid #797979; +} + +.noUi-vertical { + height: 200px; + margin: 15px auto 10px; +} diff --git a/chrome/app/styles/paper.scss b/chrome/app/styles/paper.scss new file mode 100644 index 0000000..901c2b4 --- /dev/null +++ b/chrome/app/styles/paper.scss @@ -0,0 +1,72 @@ +@import 'ember-paper'; + +.paper-icon { + cursor: pointer; +} + +md-checkbox .md-icon, .md-off, .md-on { + border-color: inherit !important; +} + +md-checkbox.md-default-theme.md-checked .md-icon { + background: $secondaryThemeColor; +} + +md-checkbox .md-label { + width: 125px; + text-align: left; +} + +.md-button { + flex-direction: unset; + span { + width: 100%; + } +} + +md-switch[disabled=disabled], md-switch[disabled=disabled] .md-container, md-slider[disabled=disabled] { + cursor: not-allowed; +} + +md-progress-circular { + margin: 0 auto 20px auto !important; +} + +md-progress-linear { + margin-bottom: 50px !important; +} + +md-slider { + cursor: pointer; +} + +.md-thumb-text { + user-select: none; +} + +md-slider.md-default-theme .md-thumb:after { + border-color: $secondaryThemeColor; + background-color: $secondaryThemeColor; +} + +md-icon { + color: rgba(0, 0, 0, 0.54) !important; +} + +md-switch.md-default-theme.md-checked .md-thumb { + background-color: $secondaryThemeColor; +} + +.ember-basic-dropdown-trigger { + outline: none !important; +} + +md-list-item { + margin-bottom: 2vh; +} + +@media(max-width:500px) { + #save-beat-preferences-star { + right: 5px; + } +} diff --git a/chrome/bower.json b/chrome/bower.json new file mode 100644 index 0000000..66eb724 --- /dev/null +++ b/chrome/bower.json @@ -0,0 +1,14 @@ +{ + "name": "huegasm", + "dependencies": { + "JavaScript-ID3-Reader": "https://github.com/aadsm/JavaScript-ID3-Reader.git", + "bootstrap-sass": "^3.3.5", + "hammer.js": "^2.0.8", + "intro.js": "^2.1.0", + "jquery-mousewheel": "^3.1.13", + "locallyjs": "^0.3.2", + "matchMedia": "^0.3.0", + "nouislider": "^9.0.0", + "velocity": "^1.3.1" + } +} diff --git a/chrome/config/environment.js b/chrome/config/environment.js new file mode 100644 index 0000000..a683d19 --- /dev/null +++ b/chrome/config/environment.js @@ -0,0 +1,47 @@ +/* jshint node: true */ + +module.exports = function (environment) { + var ENV = { + modulePrefix: 'huegasm', + podModulePrefix: 'huegasm/pods', + environment: environment, + rootURL: '', + locationType: 'hash', + EmberENV: { + FEATURES: { + // Here you can enable experimental features on an ember canary build + // e.g. 'with-controller': true + } + }, + + APP: { + // Here you can pass flags/options to your application instance + // when it is created + } + }; + + if (environment === 'development') { + // ENV.APP.LOG_RESOLVER = true; + // ENV.APP.LOG_ACTIVE_GENERATION = true; + // ENV.APP.LOG_TRANSITIONS = true; + // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; + // ENV.APP.LOG_VIEW_LOOKUPS = true; + } + + if (environment === 'test') { + // Testem prefers this... + ENV.locationType = 'none'; + + // keep test console output quieter + ENV.APP.LOG_ACTIVE_GENERATION = false; + ENV.APP.LOG_VIEW_LOOKUPS = false; + + ENV.APP.rootElement = '#ember-testing'; + } + + if (environment === 'production') { + + } + + return ENV; +}; diff --git a/chrome/ember-cli-build.js b/chrome/ember-cli-build.js new file mode 100644 index 0000000..85f25c8 --- /dev/null +++ b/chrome/ember-cli-build.js @@ -0,0 +1,29 @@ +/* global require, module */ +var EmberApp = require('ember-cli/lib/broccoli/ember-app'); +var Funnel = require('broccoli-funnel'); + +module.exports = function (defaults) { + var app = new EmberApp(defaults, { + fingerprint: { + enabled: false + } + }); + var extraAssets = new Funnel('bower_components/bootstrap-sass/assets/fonts/bootstrap/', { + srcDir: '/', + include: ['**'], + destDir: '/fonts/bootstrap' + }); + + app.import('vendor/dancer.js'); + + app.import('bower_components/bootstrap-sass/assets/javascripts/bootstrap/tooltip.js'); + app.import('bower_components/intro.js/intro.js'); + app.import('bower_components/intro.js/introjs.css'); + app.import('bower_components/intro.js/themes/introjs-nassim.css'); + app.import('bower_components/JavaScript-ID3-Reader/dist/id3-minimized.js'); + app.import('bower_components/jquery-mousewheel/jquery.mousewheel.js'); + app.import('bower_components/locallyjs/dist/locally.min.js'); + app.import('bower_components/velocity/velocity.js'); + + return app.toTree(extraAssets); +}; diff --git a/chrome/package.json b/chrome/package.json new file mode 100644 index 0000000..b05c55d --- /dev/null +++ b/chrome/package.json @@ -0,0 +1,46 @@ +{ + "name": "huegasm", + "version": "1.0.0", + "description": "Huegasm is a free web application for managing and synchronizing your Philips Hue lights with the beat of your music.", + "private": true, + "directories": { + "doc": "doc", + "test": "tests" + }, + "scripts": { + "start": "ember server --live-reload=false", + "build": "ember build --env=production" + }, + "engines": { + "node": ">= 0.12.0" + }, + "author": "Egor Philippov", + "license": "MIT", + "devDependencies": { + "broccoli-asset-rev": "^2.2.0", + "ember-ajax": "^2.0.1", + "ember-cli": "^2.8.0", + "ember-cli-app-version": "^2.0.0", + "ember-cli-babel": "^5.1.5", + "ember-cli-dependency-checker": "^1.2.0", + "ember-cli-htmlbars": "^1.0.1", + "ember-cli-htmlbars-inline-precompile": "^0.3.1", + "ember-cli-inject-live-reload": "^1.3.1", + "ember-cli-nouislider": "^0.11.0", + "ember-cli-release": "0.2.8", + "ember-cli-shims": "^1.0.2", + "ember-cli-sass": "^6.0.0", + "ember-cli-sri": "^2.1.0", + "ember-cli-test-loader": "^1.1.0", + "ember-cli-uglify": "^1.2.0", + "ember-export-application-global": "^1.0.4", + "ember-load-initializers": "^0.6.3", + "ember-modal-dialog": "^0.9.0", + "ember-notify": "^5.0.4", + "ember-paper": "^1.0.0-alpha.14", + "ember-resolver": "^2.0.3", + "ember-truth-helpers": "^1.2.0", + "ember-source": "^2.11.0", + "loader.js": "^4.0.7" + } +} \ No newline at end of file diff --git a/chrome/public/128x128.png b/chrome/public/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..33ec2e7792f98f466f40eb71069b645eb36b38b8 GIT binary patch literal 9408 zcmV;xBtP4UP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 Rn&usFFznCK>umk`{{#Al(UbrH literal 0 HcmV?d00001 diff --git a/chrome/public/48x48.png b/chrome/public/48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..8c41674b557ee15bce2f8215963c40bf2a40b09e GIT binary patch literal 3199 zcmV-_41n{AP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 literal 0 HcmV?d00001 diff --git a/chrome/public/assets/images/colormap.png b/chrome/public/assets/images/colormap.png new file mode 100644 index 0000000000000000000000000000000000000000..53c80efa728bfb6c76c68d6ab3fc4a00159ebc30 GIT binary patch literal 6305 zcmX9@c_38l8$M$h`@RifER{;KW@oHfqms1HSh7?%6lF;>ma>$rS7aNZD_6E#8ZFF> zT@gZ5m|NKxE6Q2KiCogAoeZ}w&!Di zu1-G&oclT>FWlE zCBW|by77f|g+dcx^D>)9IIu7=F)w@+{1%leJzmHBh`G1NIR@O%M#ik7+qD^Hn*1Lc!Vtfv8(ivTUC&wPSZmHd&Vi)-AJ;rHK!%uThwa#u*KXTO`` z~2+w zG=Y+KAr0PP<8Ap&jhz~R9VGf!H zXr|>;ioyj*Ba-3wtD9|hm^uTfM4!hF?@+430pJP+2@m}76N^^)gPgfD>4Auc^eA$C zJ~9urB6xpFB?jJVq@YofYdtPx8ZI|1nme|@OdLHnrDwrs3%X5U8$&KEKz#|W3Rkxbje5s+c+h@`{S<71xrUNdl zpUu$)61vkG+3wVo>>&X*P%pn;$6_wEL-1WIwwcIEIp-U4n#DL8iJ}wWt`~{MfM-T% zGDP$-T>iARZ3f8hLyvC(L52Nnxske1vsa(hbV`)(`h^2M)EM@VXDKxyny-v-Jv_wh&8 zz#d>hR{QU_>rr2NmfrBvTyJz(?Ke8#R*3aqvBkE!I}=#Xz1dyrl_b4pBfB@>O(Z!% z>uJ76Z52hS#>l9>F9a&BO}mS%735&iDjOp-!t~TxNE?L}+)Xj$PEpUvLWVItwNyACDb}^>W z-Z1$+x48*A64`W4py+jFdc9hHfo5PMzYQuXrN(?XhLr>w@t+X$N@sX6gPvMFb4!CtpOxu0#)Qal8RFBgGyMzH_?#%kDOZt*TAc7mPUa3EHzJT3w0dY(;Ym z-(r#Q@REG{+h`2LdQ>9#2;XyPkhx1D-7<>AyypA?O1OqRY(2Lsv8!Dx)vK7-=im{Q zt0wcisQ-jtt<=zV=-d@6q?R@xG?Cv77oQth-A^>YTwQB?3SX~Dv$GktA@!tS2PnD8_`D75*g8`f&Q;H3sv-y~SyoC8pKk+eOQEgNy2lmO1$BxYdS%$X>eb zI4FcXO#a0z%hJ0@fx!8miKQ*Xc zQ*a5?a(16^Gvj8=h4lK(rB38l36y}2PY!Y;BtOCap2sf>HLN)$VrJ%Hhma(`J`IXYz>UODG)LnW@T&Ov*gsw`!y?qfdJdb1L z?LkZpbRG}kXXY9o;t3l=f>9Cv-dhlu@UtIHArA|-(Xkviavif_q+&qu)`lDqrHe99 zibg>x`>4cBikeRrDhjR4G~M=jAn9cL?)u_jMKx4+a{?Q&rTwKkxm;QJW=XrC&2vT9 zugMbvoi9DJPH#_i*6dkyzGybE!bXb*Zh|sX?HSD2D$GGd3;gs`K`ZIgVSFK~lYi?K z^#^v1rB`FxMqt@aqKI#JSy-2A({rQOF{SZenk*b4NJ9J})8XtJ?6`z2%&#N_jrskI z;9q^hLMy?6`41ODx{ntgJSmBvqvT=Cn4JTWAsv3vJ>6gDd%|5IJ&G@$A6Av!Mq48F zKXvK`KQ6xxRuSOs5(XDN2ECr%yVG4&0gYql#r8{dd)|APR00a{$SNu^!F_6qJ7c>v z@&GYL_3754r`6jmgbKb65?SNZqSl%>4da&?Ux*WX8Ao&1ts3a(B<{t#{+?y~%o15M zW;k!me+zslBK~Q_eU?=I9Qvy>lCHi=-N+yEZRu!lL))DEf}GJAc4OY^*EvIYaf@TK zismu<9NM29zjeuU($jL~5zU$7tbD`@X~9+Ea!1LVsxDdg~2 znBchBKXNw8IdQMygBc}{P(lyy8@;-8N3T%VRMraF$9f^DKtJ%H#T|!MgB_^I;5XJZjlPus@vHf;dPlu=V7hDeYn*{C@n+s5_`KGKFYx-V$Ytk=e)?>F2x5buU)JnC_ zURwTTuaM9yqOD~GQ~tTutIg~t8#b;{V$3`25T}s*s%3X2d<}$eGWY%G$|ZH_6sbE1 zC#?|E!Tq=UECbAzZ@=9wAKwmgQ6ab^4<(FQN%-CcTGlnaJ<@-QBW3!b;$G?<)9i## zXmF++BHS9lAC#Yfd{50Sy3Q&tmx%lu2E`wvr(II_f3Nk?-nZ;ULZR9Zm1H!7^daT( z>>JjF_~U z0c62q3F)UpWItA@OirEQO}WLsfJ0@%&8)u@N!B3$)5eJgntfjmbuT*tF$|S_(0AfA zf^J`tFYj0^tfUVW-xD#4w=TU~f^309&HfYjJQtwyh@kMsF2{xE^5k012knXHl6jEX zdUi)_?V<&%OCxSLiccpDgqV7E2iJNzspn~!7VXLGY2=fgs4Y+h=h(fpS4VS|1?J0S zb(8QU-9#U#>Jn5uEmd1nwbX@TQ*~d8`qD+zRp7_fW!;RSt^056-7cMb?@-&8iB;>c z>U)%4d!fP+F-F-ZG#Qa=70?Wj-&i&;vG18xUmob*kuGy&9dTchNCuty76$fqd=-}h z2osKpk=_=4_-jZ-rYf>VaqN>W!CnJ#IHwWUo6@SL{5x%RpKj6eeQFGseqMRl?pPt! ztm%9996O$N9@%A%%lCvcH>I{fl`|=i**|HMBIJRv;exK0vl-a#TM%jC5|0&!-2q+ri5RMoWs5R>$@90ZZ)wYRODz?kcO4HIN7H=#iPx7o^v7x zXH*$*Liy#N$!J5`B_V3$IXjPL$%)DRbQOlxVMkh8(r&Rg3~I5{?8t;Iq6rNn63iRJ z*xz~?A~6Sb^C!+8ahU~lGjF}%h()mNIFY7r8)+qJYxDn7)K>1j+UT`I%Bt#fMeLkF zSp1GtEuYJdT6#>f)ZIiw)>)eI6FiZ$z?&ntJ{+Q#|3%qxI3Y2g7KPs<3cj6JK9^W)9JqHsj{xs3y2HZ#-2a2SlO6)frg4r3^rJZ1s5 zU&@V?$gG4$zQvnY8B*%Y@h6pJXPz!b0bS{n+&+TOJK4anqKFSWQT3=-d?_V=VfnBp zA?}a<4d5PhJhsibdhh)}B%uM!G-^HkanNtXFj3_>msa-S8CBpPl5F(%t$B^%*4S53 zvjwh^-bfMhzS?%bmP~if9kC3@v#Z|N$!=+4QJK_oY8`WX4|r;w*{x!G4N`P`;`1J$ zyDcy%klu|BWP4ne>D7cJ%sGuH2jv=X+mW z;TA0-0u)Q?wr&7QiTEQC{LGmv6%R-~{zIrwS1O{I$J((vUe#PD)-(0K8DLRPb&qpZ zkK;{?g*7|RxJO`p^}vg^=fSzdqgAMl#>Z)QdKj$lkzsHoIT~3(Z%3y4=jhwyK~J)F z$hC;TQ;;8{?)QSC_9UdFy_iePv!zn<^ARWa;f%O>gfGk-F8WU+*os;cHDs9uTHjUZ ze!9rDTrrOjYofkjf^ac%cO#A@LQ-kx^`>flZ$BN06jg6g&PPc_im~W!yUoH4F4tBF=qveJ zNTr?1_Ds2InM!IaRz_!Z#QZ5a3jt8UftNWpzG9)q{@c6{`R4f9g#;hE#r3l^k&f1) zm5C%<5EPbZI)eN`%lua9`@8WWbjw7?k)6`yo6!* zF|l;P+;M=7lDKV9@iv-_1niDva6a_4_`iTN+IzI zv$3F;mf@;**qXm>0@UuiCGlMZE5}w$xRUI-!xiLe3)>8BaRK8hp3765iE*|oO$-sP zH`S^G_-;*hT`aNo*0m8!ufS^wwWsx^N7H6pA^A{(xU}f zkV++cskLFZu!}^0e?@Ir#;!&y&EQRd?L(aT(#tCq`CM!r*f+-M9zw&+U=GQ+HAl3P zN7Hah?vKB3bwAcM<@|z*#n2()q)ZYF^p!f%!wrtSfq|=%7cs@Srs79~LQ+i~WQdSx z3Wfesec>v14)cJOsjB>l(?EI^uQ%^dbUi~kciH3I8;(pxxji*?&a)Eiv1ufAgVqpJ zouScTJ`5k?(q>a{#13DIhXTi0A-|d$m?6)Xk%GIYI zD2pfTy}V0)5!-?}Xg$Oklj@P(-BzqxpU||{ZQR9xFvA;uS~M&8>GsRh^Dw0d1~<_7 z-SgMr`21B)9hrh?185pbp{y>a#s7m2!@t@6Pk+7IKP|US9*m%EYUuk!*pQ`XXdH#E zzQ#N0l;N~h%IibpPQ^4vP?G=qX{)5u-`&cRBpJNi{uD)Ib307rc7J486$s2Ce2dHh zQSHlkW9T;tXnLAVVEi7aO)4m$z;!q+N&*DXrq*PpuFUwi!JL3;pu+=un{1@N^6W+7<=>#Dao!>-u(4D zKay_h(5?(WRy42wX)71s9qvi@WV>uE7kU@ev zw5&Q@aTGxw8OOZL$>y<1P1EP^yuJzh+l1wg6k{55vfmylx~L$t9Y5O;q8D;^ z0N?6s*|6)=5kcl=oPExTS5*wd%Y3&$PuodxAH>=x_J?xhF(3(6b%4hXA-yr?_(xp) z_|^0wQY4c%`S<^*Pynb%2%P>?(q~PJsNc%aRI*#&QRob1Sn%8YyFlJS)*b6QE-zlm zZbb7;bK_P#k#iPE?1Piy`RT@b?Jfh{PyMOah5}&2u;6Uhzz02GEx%GsH)i>j=XDeD zOIpH-nwF6v{8&BxL^ELrD7Yq2eEZM7>j7FvMff5Lxqh-^srjrA!)-uycl6%BcE-`JZ?ndp@-NUXsx>eMr?Ul`<;K#9j3`U@ktS6Vh#7@J@*GKMf?zH>~RjiRr^|5guX6_T*XYPR4 znC7{I9zr4G&DVKQm0kW!-3_n*)6p*yo>T7kJt*E=GUlMoaYaW<)(zw><{ggLAyYot zpJ3C9B9Yn_tA8hGH;ILeZl0#{x^?_w1q`Vo7zx%i i!~;w?YYzqj1M?pfmQ;k^+~)ps09+hB92)HWlm7?Zp2N@p literal 0 HcmV?d00001 diff --git a/chrome/public/assets/images/google-play-badge.png b/chrome/public/assets/images/google-play-badge.png new file mode 100644 index 0000000000000000000000000000000000000000..585006ba67325d936f202ac2dd7ec385dbdb2cfa GIT binary patch literal 4219 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

6+Ce#m#lwe`wskBX z9v-;y@i_3SN7~v~V!5=sTG-mkN<~FQ0fiba)({H1y1HIID8@}p#J9A_{@b3To=4LB zTf@VE&V`=%S6FO<;FA(-{5wsG8#hP*(7f4w%T)x>peNx-6VM?8KKJ(@x+$L4Smz6f z)I$aCSt~6kuQdiMieDpmVJm4f8$%*yfWg|{%&Y^Skuvw|Lpv{&f{m3 z<gmw}o!`D~e<}KG8x#^!*wnIJ{>cv>J|_g21v5KsDp0^M7zvTwT#yzvM9N$Z{d)9wcTv=RYWo35daCVh%d=Gtm#7A;uqf?bHSmXn@H2nRg zs5s=tI!HI)BpNAE)g#Pi-Y-HVtn?>hL(gl9zJ-Q+NaQw6LjrfLZEbn6?eE_4idwbj zfXtVim}p~adJ{>&6{xHITv>6%EMfQ_f(sc;PQHOz0u%EZ+Qr2M{^e81;V4)TfCicy znX4RONl}X+jJHR)r(CNDCDQ!xuvKpx~397{?{lAk~4MQ;82WbQ$g z7k(m$^c}A>{WPM-pLWeBGCCT9Bo9~4T$HHZ%V|}H@Z#yJAGGp zse~;$KX{N~N`iq|!oubpa?cG;Hf~Z6iXUEy8as|~@RWFv8m(>{B9KJG1aba_FtNSX-%bybw5xL%d zPu!TFpKrN(IQmem4pi~u<6{zTcV3LUv-AA&*EdtMv)U065e46dv!#)knl{29AT%kB z=9Vq_O|+r)SO8j5KeVacF7~5vvz-0?eHKp6k%0jO zDkw=;O!KIalT^z3>5Qrng^VbjfXc!p{lP)12_er4&Rc4Llj};t;u*!d0&Mv{42s$}E zE%ce6pWg+nc{p5yEv?bwUcVI9Ha1F~Ejz-`dye;3PeB@**x&c_3kXny(=GIJbzYjx zFH&ECes5(cGrG36mQwlRXpxNeiV!xkGcz^){rrYPcIG1^5pewk)I}D7ID+OS1JO%t qR+iEz9xrR4r&kwpu>ON^Nvczv{%+vMNyycI0Ho$)jcPUPsQ&@NK?H*U literal 0 HcmV?d00001 diff --git a/chrome/public/assets/images/huegasm.png b/chrome/public/assets/images/huegasm.png new file mode 100644 index 0000000000000000000000000000000000000000..649034280ef5399823e8ae167c4e8e7d55c3ba4d GIT binary patch literal 3876 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 literal 0 HcmV?d00001 diff --git a/chrome/public/assets/images/lights/a19.svg b/chrome/public/assets/images/lights/a19.svg new file mode 100644 index 0000000..cf3eee3 --- /dev/null +++ b/chrome/public/assets/images/lights/a19.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/a19w.svg b/chrome/public/assets/images/lights/a19w.svg new file mode 100644 index 0000000..b05e873 --- /dev/null +++ b/chrome/public/assets/images/lights/a19w.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/br30.svg b/chrome/public/assets/images/lights/br30.svg new file mode 100644 index 0000000..7d71208 --- /dev/null +++ b/chrome/public/assets/images/lights/br30.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/br30w.svg b/chrome/public/assets/images/lights/br30w.svg new file mode 100644 index 0000000..ba1585a --- /dev/null +++ b/chrome/public/assets/images/lights/br30w.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/gu10.svg b/chrome/public/assets/images/lights/gu10.svg new file mode 100644 index 0000000..014d2f5 --- /dev/null +++ b/chrome/public/assets/images/lights/gu10.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/gu10w.svg b/chrome/public/assets/images/lights/gu10w.svg new file mode 100644 index 0000000..1ea01cb --- /dev/null +++ b/chrome/public/assets/images/lights/gu10w.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/huego.svg b/chrome/public/assets/images/lights/huego.svg new file mode 100644 index 0000000..e912e5a --- /dev/null +++ b/chrome/public/assets/images/lights/huego.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/huegow.svg b/chrome/public/assets/images/lights/huegow.svg new file mode 100644 index 0000000..b3b0b88 --- /dev/null +++ b/chrome/public/assets/images/lights/huegow.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/lc_aura.svg b/chrome/public/assets/images/lights/lc_aura.svg new file mode 100644 index 0000000..9faf3b9 --- /dev/null +++ b/chrome/public/assets/images/lights/lc_aura.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/lc_auraw.svg b/chrome/public/assets/images/lights/lc_auraw.svg new file mode 100644 index 0000000..c125947 --- /dev/null +++ b/chrome/public/assets/images/lights/lc_auraw.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/lc_bloom.svg b/chrome/public/assets/images/lights/lc_bloom.svg new file mode 100644 index 0000000..a498b9a --- /dev/null +++ b/chrome/public/assets/images/lights/lc_bloom.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/lc_bloomw.svg b/chrome/public/assets/images/lights/lc_bloomw.svg new file mode 100644 index 0000000..3dce9f7 --- /dev/null +++ b/chrome/public/assets/images/lights/lc_bloomw.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/lc_iris.svg b/chrome/public/assets/images/lights/lc_iris.svg new file mode 100644 index 0000000..876b3d2 --- /dev/null +++ b/chrome/public/assets/images/lights/lc_iris.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/lc_irisw.svg b/chrome/public/assets/images/lights/lc_irisw.svg new file mode 100644 index 0000000..cdf472b --- /dev/null +++ b/chrome/public/assets/images/lights/lc_irisw.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/lightstrip.svg b/chrome/public/assets/images/lights/lightstrip.svg new file mode 100644 index 0000000..1d55b8e --- /dev/null +++ b/chrome/public/assets/images/lights/lightstrip.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/lightstripw.svg b/chrome/public/assets/images/lights/lightstripw.svg new file mode 100644 index 0000000..fb32dab --- /dev/null +++ b/chrome/public/assets/images/lights/lightstripw.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/storylight.svg b/chrome/public/assets/images/lights/storylight.svg new file mode 100644 index 0000000..d04a5db --- /dev/null +++ b/chrome/public/assets/images/lights/storylight.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/lights/storylightw.svg b/chrome/public/assets/images/lights/storylightw.svg new file mode 100644 index 0000000..8bc66a5 --- /dev/null +++ b/chrome/public/assets/images/lights/storylightw.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/chrome/public/assets/images/logo.png b/chrome/public/assets/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e384e771737b508919b34a8452047a7a61d1b352 GIT binary patch literal 28287 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 FV1oB7oW=kE literal 0 HcmV?d00001 diff --git a/chrome/public/assets/images/pressButtonBridge.png b/chrome/public/assets/images/pressButtonBridge.png new file mode 100644 index 0000000000000000000000000000000000000000..e98ac238d3d28c04924aae8d60bbb149c955562e GIT binary patch literal 7050 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

  • d$H~fe`_16QE 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 Y0AkG>lKd;wga7~l07*qoM6N<$f)k2Mw*UYD literal 0 HcmV?d00001 diff --git a/chrome/public/manifest.json b/chrome/public/manifest.json new file mode 100644 index 0000000..7eed707 --- /dev/null +++ b/chrome/public/manifest.json @@ -0,0 +1,28 @@ +{ + "manifest_version": 2, + "name": "Huegasm", + "description": "Huegasm is a free web application for managing and synchronizing your Philips Hue lights with the beat of your music.", + "version": "1.0", + "content_security_policy": "script-src https://connect.soundcloud.com 'self'; object-src 'self'", + "icons": { + "16": "16x16.png", + "48": "48x48.png", + "128": "128x128.png" + }, + "background": { + "page": "index.html" + }, + "browser_action": { + "default_icon": { + "16": "16x16.png", + "32": "32x32.png" + }, + "default_popup": "index.html", + "default_title": "Huegasm" + }, + "permissions": [ + "activeTab", + "background", + "https://ajax.googleapis.com/" + ] +} \ No newline at end of file diff --git a/chrome/vendor/dancer.js b/chrome/vendor/dancer.js new file mode 100644 index 0000000..72d2894 --- /dev/null +++ b/chrome/vendor/dancer.js @@ -0,0 +1,709 @@ +/* + * dancer - v0.4.0 - 2014-02-01 + * https://github.com/jsantell/dancer.js + * Copyright (c) 2014 Jordan Santell + * Licensed MIT + */ +(function() { + + var Dancer = function () { + this.audioAdapter = Dancer._getAdapter( this ); + this.events = {}; + this.sections = []; + this.bind( 'update', update ); + }; + + Dancer.version = 'X.X.X'; + Dancer.adapters = {}; + + Dancer.prototype = { + + load : function ( source, micBoost, useMic ) { + // Loading an Audio element + if ( source instanceof HTMLElement ) { + this.source = source; + // Loading an object with src, [codecs] + } else if(source instanceof EventTarget){ + this.source = source; + } else { + this.source = window.Audio ? new Audio() : {}; + this.source.src = Dancer._makeSupportedPath( source.src, source.codecs ); + } + + this.useMic = useMic === true; + this.boost = micBoost ? micBoost : 1; + this.audio = this.audioAdapter.load(this.source, this.useMic, this.boost); + + return this; + }, + + /* Controls */ + play : function () { + this.audioAdapter.play(); + return this; + }, + + pause : function () { + this.audioAdapter.pause(); + return this; + }, + + setVolume : function ( volume ) { + this.audioAdapter.setVolume( volume ); + return this; + }, + + setBoost : function ( boost ) { + this.audioAdapter.setBoost( boost ); + return this; + }, + + /* Actions */ + createKick : function ( options ) { + return new Dancer.Kick( this, options ); + }, + + bind : function ( name, callback ) { + if ( !this.events[ name ] ) { + this.events[ name ] = []; + } + this.events[ name ].push( callback ); + return this; + }, + + unbind : function ( name ) { + if ( this.events[ name ] ) { + delete this.events[ name ]; + } + return this; + }, + + trigger : function ( name ) { + var _this = this; + if ( this.events[ name ] ) { + this.events[ name ].forEach(function( callback ) { + callback.call( _this ); + }); + } + return this; + }, + + + /* Getters */ + + getVolume : function () { + return this.audioAdapter.getVolume(); + }, + + getProgress : function () { + return this.audioAdapter.getProgress(); + }, + + getTime : function () { + return this.audioAdapter.getTime(); + }, + + // Returns the magnitude of a frequency or average over a range of frequencies + getFrequency : function ( freq, endFreq ) { + var sum = 0; + if ( endFreq !== undefined ) { + for ( var i = freq; i <= endFreq; i++ ) { + sum += this.getSpectrum()[ i ]; + } + return sum / ( endFreq - freq + 1 ); + } else { + return this.getSpectrum()[ freq ]; + } + }, + + getWaveform : function () { + return this.audioAdapter.getWaveform(); + }, + + getSpectrum : function () { + return this.audioAdapter.getSpectrum(); + }, + + isLoaded : function () { + return this.audioAdapter.isLoaded; + }, + + isPlaying : function () { + return this.audioAdapter.isPlaying; + }, + + + /* Sections */ + + after : function ( time, callback ) { + var _this = this; + this.sections.push({ + condition : function () { + return _this.getTime() > time; + }, + callback : callback + }); + return this; + }, + + before : function ( time, callback ) { + var _this = this; + this.sections.push({ + condition : function () { + return _this.getTime() < time; + }, + callback : callback + }); + return this; + }, + + between : function ( startTime, endTime, callback ) { + var _this = this; + this.sections.push({ + condition : function () { + return _this.getTime() > startTime && _this.getTime() < endTime; + }, + callback : callback + }); + return this; + }, + + onceAt : function ( time, callback ) { + var + _this = this, + thisSection = null; + this.sections.push({ + condition : function () { + return _this.getTime() > time && !this.called; + }, + callback : function () { + callback.call( this ); + thisSection.called = true; + }, + called : false + }); + // Baking the section in the closure due to callback's this being the dancer instance + thisSection = this.sections[ this.sections.length - 1 ]; + return this; + } + }; + + function update () { + for (var i in this.sections) { + if (this.sections[i].condition && this.sections[i].condition() ) + this.sections[i].callback.call(this); + } + } + + window.Dancer = Dancer; +})(); + +(function ( Dancer ) { + + var CODECS = { + 'mp3' : 'audio/mpeg;', + 'ogg' : 'audio/ogg; codecs="vorbis"', + 'wav' : 'audio/wav; codecs="1"', + 'aac' : 'audio/mp4; codecs="mp4a.40.2"' + }, + audioEl = document.createElement( 'audio' ); + + Dancer.options = {}; + + Dancer.setOptions = function ( o ) { + for ( var option in o ) { + if ( o.hasOwnProperty( option ) ) { + Dancer.options[ option ] = o[ option ]; + } + } + }; + + Dancer.isSupported = function () { + if ( !window.Float32Array || !window.Uint32Array ) { + return null; + } else if ( !isUnsupportedSafari() && ( window.AudioContext || window.webkitAudioContext )) { + return 'webaudio'; + } else { + return ''; + } + }; + + Dancer.canPlay = function ( type ) { + var canPlay = audioEl.canPlayType; + return !!( + type.toLowerCase() === 'mp3' || + audioEl.canPlayType && + audioEl.canPlayType( CODECS[ type.toLowerCase() ] ).replace( /no/, '')); + }; + + Dancer.addPlugin = function ( name, fn ) { + if ( Dancer.prototype[ name ] === undefined ) { + Dancer.prototype[ name ] = fn; + } + }; + + Dancer._makeSupportedPath = function ( source, codecs ) { + if ( !codecs ) { return source; } + + for ( var i = 0; i < codecs.length; i++ ) { + if ( Dancer.canPlay( codecs[ i ] ) ) { + return source + '.' + codecs[ i ]; + } + } + return source; + }; + + Dancer._getAdapter = function ( instance ) { + switch ( Dancer.isSupported() ) { + case 'webaudio': + return new Dancer.adapters.webaudio( instance ); + default: + return null; + } + }; + + Dancer._getMP3SrcFromAudio = function ( audioEl ) { + var sources = audioEl.children; + if ( audioEl.src ) { return audioEl.src; } + for ( var i = sources.length; i--; ) { + if (( sources[ i ].type || '' ).match( /audio\/mpeg/ )) return sources[ i ].src; + } + return null; + }; + + // Browser detection is lame, but Safari 6 has Web Audio API, + // but does not support processing audio from a Media Element Source + // https://gist.github.com/3265344 + function isUnsupportedSafari () { + var + isApple = !!( navigator.vendor || '' ).match( /Apple/ ), + version = navigator.userAgent.match( /Version\/([^ ]*)/ ); + version = version ? parseFloat( version[ 1 ] ) : 0; + return isApple && version <= 6; + } + +})( window.Dancer ); + +(function ( undefined ) { + var Kick = function ( dancer, o ) { + o = o || {}; + this.dancer = dancer; + this.frequency = o.frequency !== undefined ? o.frequency : [ 0, 5 ]; + this.threshold = o.threshold !== undefined ? o.threshold : 0.3; + this.decay = o.decay !== undefined ? o.decay : 0.02; + this.onKick = o.onKick; + this.offKick = o.offKick; + this.isOn = false; + this.currentThreshold = this.threshold; + this.previousMag = 0; + this.canUseRatio = true; + this.canUseRatioHandle = null; + + var _this = this; + this.dancer.bind( 'update', function () { + _this.onUpdate(); + }); + }; + + Kick.prototype = { + on : function () { + this.isOn = true; + return this; + }, + off : function () { + this.isOn = false; + return this; + }, + + set : function ( o ) { + o = o || {}; + this.frequency = o.frequency !== undefined ? o.frequency : this.frequency; + this.threshold = o.threshold !== undefined ? o.threshold : this.threshold; + this.decay = o.decay !== undefined ? o.decay : this.decay; + this.onKick = o.onKick || this.onKick; + this.offKick = o.offKick || this.offKick; + }, + + onUpdate : function () { + if ( !this.isOn ) { return; } + + var magnitude = this.maxAmplitude(this.frequency); + + if (magnitude >= this.currentThreshold && magnitude >= this.threshold) { + this.currentThreshold = magnitude; + this.onKick && this.onKick.call(this.dancer, magnitude); + this.canUseRatio = false; + + if(this.canUseRatioHandle) { + clearTimeout(this.canUseRatioHandle); + this.canUseRatioHandle = null; + } + + var self = this; + this.canUseRatioHandle = setTimeout(function(){ + self.canUseRatio = true; + }, 5000); + } else { + if(magnitude/this.previousMag > this.threshold*5 && magnitude>0.1 && this.canUseRatio) { + this.onKick && this.onKick.call(this.dancer, magnitude, magnitude/this.previousMag); + } else { + this.offKick && this.offKick.call(this.dancer, magnitude); + } + + this.currentThreshold -= this.decay; + this.previousMag = (magnitude > 0) ? magnitude : 0.0001; + } + }, + maxAmplitude : function ( frequency ) { + var max = 0, fft = this.dancer.getSpectrum(); + + // Sloppy array check + if ( !frequency.length ) { + return frequency < fft.length ? + fft[ ~~frequency ] : + null; + } + + for ( var i = frequency[ 0 ], l = frequency[ 1 ]; i <= l; i++ ) { + if ( fft[ i ] > max ) { max = fft[ i ]; } + } + return max; + } + }; + + window.Dancer.Kick = Kick; +})(); + +(function() { + var + SAMPLE_SIZE = 2048, + SAMPLE_RATE = 44100; + + var adapter = function ( dancer ) { + var context; + + if('AudioContext' in window) { + context = new AudioContext(); + } else { + context = new webkitAudioContext(); + } + + this.dancer = dancer; + this.audio = new Audio(); + this.context = context; + }; + + adapter.prototype = { + + load : function (_source, useMic, boost) { + var _this = this; + this.audio = _source; + this.useMic = useMic; + this.boost = boost; + + this.isLoaded = false; + this.progress = 0; + + if(this.proc){ + this.proc.onaudioprocess = null; + delete this.proc; + } + + this.proc = this.context.createScriptProcessor( SAMPLE_SIZE / 2, 1, 1 ); + + this.proc.onaudioprocess = function ( e ) { + _this.update.call( _this, e ); + }; + + this.gain = this.context.createGain(); + + this.fft = new FFT( SAMPLE_SIZE / 2, SAMPLE_RATE, this.boost ); + this.signal = new Float32Array( SAMPLE_SIZE / 2 ); + + if ( this.audio.readyState < 3 ) { + this.audio.addEventListener( 'canplay', function () { + connectContext.call( _this ); + }); + } else { + connectContext.call( _this ); + } + + this.audio.addEventListener( 'progress', function ( e ) { + if ( e.currentTarget.duration && e.currentTarget.duration !== Infinity ) { + _this.progress = e.currentTarget.seekable.end( 0 ) / e.currentTarget.duration; + } + }); + + return this.audio; + }, + + play : function () { + this.audio.play(); + this.isPlaying = true; + }, + + pause : function () { + this.audio.pause(); + this.isPlaying = false; + }, + + setVolume : function ( volume ) { + this.gain.gain.value = volume; + }, + + setBoost : function( boost ){ + if(this.fft){ + this.fft.setBoost(boost); + } + + this.boost = boost; + }, + + getVolume : function () { + return this.gain.gain.value; + }, + + getProgress : function() { + return this.progress; + }, + + getWaveform : function () { + return this.signal; + }, + + getSpectrum : function () { + return this.fft.spectrum; + }, + + getTime : function () { + return this.audio.currentTime; + }, + + update : function ( e ) { + if ((!this.isPlaying || !this.isLoaded) && this.useMic !== true ) return; + + var + buffers = [], + channels = e.inputBuffer.numberOfChannels, + resolution = SAMPLE_SIZE / channels, + sum = function ( prev, curr ) { + return prev[ i ] + curr[ i ]; + }, i; + + for ( i = channels; i--; ) { + buffers.push( e.inputBuffer.getChannelData( i ) ); + } + + for ( i = 0; i < resolution; i++ ) { + this.signal[ i ] = channels > 1 ? + buffers.reduce( sum ) / channels : + buffers[ 0 ][ i ]; + } + + this.fft.forward( this.signal ); + this.dancer.trigger( 'update' ); + } + }; + + function connectContext () { + try { + if(this.useMic){ + this.source = this.context.createMediaStreamSource(this.audio); + } else { + this.source = this.context.createMediaElementSource(this.audio); + } + } catch (err) { + console.info('Dancer: '+ err); + return; + } + + this.source.connect(this.proc); + this.source.connect(this.gain); + this.gain.connect(this.context.destination); + this.proc.connect(this.context.destination); + + this.isLoaded = true; + this.progress = 1; + this.dancer.trigger( 'loaded' ); + } + + Dancer.adapters.webaudio = adapter; + +})(); + + +/* + * DSP.js - a comprehensive digital signal processing library for javascript + * + * Created by Corban Brook on 2010-01-01. + * Copyright 2010 Corban Brook. All rights reserved. + * + */ + +// Fourier Transform Module used by DFT, FFT, RFFT +function FourierTransform(bufferSize, sampleRate, boost) { + this.bufferSize = bufferSize; + this.sampleRate = sampleRate; + this.bandwidth = 2 / bufferSize * sampleRate / 2; + this.boost = boost ? boost : 1; + + this.spectrum = new Float32Array(bufferSize/2); + this.real = new Float32Array(bufferSize); + this.imag = new Float32Array(bufferSize); + + this.peakBand = 0; + this.peak = 0; + + /** + * Calculates the *middle* frequency of an FFT band. + * + * @param {Number} index The index of the FFT band. + * + * @returns The middle frequency in Hz. + */ + this.getBandFrequency = function(index) { + return this.bandwidth * index + this.bandwidth / 2; + }; + + this.setBoost = function(boost){ + this.boost = boost; + }; + + this.calculateSpectrum = function() { + var spectrum = this.spectrum, + real = this.real, + imag = this.imag, + boost = this.boost, + bSi = 2 / this.bufferSize, + sqrt = Math.sqrt, + rval, + ival, + mag; + + for (var i = 0, N = bufferSize/2; i < N; i++) { + rval = real[i]; + ival = imag[i]; + mag = bSi * sqrt(rval * rval + ival * ival); + + if (mag > this.peak) { + this.peakBand = i; + this.peak = mag; + } + + spectrum[i] = mag * boost; + } + }; +} + +/** + * FFT is a class for calculating the Discrete Fourier Transform of a signal + * with the Fast Fourier Transform algorithm. + * + * @param {Number} bufferSize The size of the sample buffer to be computed. Must be power of 2 + * @param {Number} sampleRate The sampleRate of the buffer (eg. 44100) + * @param {Number} boost The coefficient + * + * @constructor + */ +function FFT(bufferSize, sampleRate, boost) { + FourierTransform.call(this, bufferSize, sampleRate, boost); + + this.reverseTable = new Uint32Array(bufferSize); + + var limit = 1; + var bit = bufferSize >> 1; + + var i; + + while (limit < bufferSize) { + for (i = 0; i < limit; i++) { + this.reverseTable[i + limit] = this.reverseTable[i] + bit; + } + + limit = limit << 1; + bit = bit >> 1; + } + + this.sinTable = new Float32Array(bufferSize); + this.cosTable = new Float32Array(bufferSize); + + for (i = 0; i < bufferSize; i++) { + this.sinTable[i] = Math.sin(-Math.PI/i); + this.cosTable[i] = Math.cos(-Math.PI/i); + } +} + +/** + * Performs a forward transform on the sample buffer. + * Converts a time domain signal to frequency domain spectra. + * + * @param {Array} buffer The sample buffer. Buffer Length must be power of 2 + * + * @returns The frequency spectrum array + */ +FFT.prototype.forward = function(buffer) { + // Locally scope variables for speed up + var bufferSize = this.bufferSize, + cosTable = this.cosTable, + sinTable = this.sinTable, + reverseTable = this.reverseTable, + real = this.real, + imag = this.imag, + spectrum = this.spectrum; + + var k = Math.floor(Math.log(bufferSize) / Math.LN2); + + if (Math.pow(2, k) !== bufferSize) { throw "Invalid buffer size, must be a power of 2."; } + if (bufferSize !== buffer.length) { throw "Supplied buffer is not the same size as defined FFT. FFT Size: " + bufferSize + " Buffer Size: " + buffer.length; } + + var halfSize = 1, + phaseShiftStepReal, + phaseShiftStepImag, + currentPhaseShiftReal, + currentPhaseShiftImag, + off, + tr, + ti, + tmpReal, + i; + + for (i = 0; i < bufferSize; i++) { + real[i] = buffer[reverseTable[i]]; + imag[i] = 0; + } + + while (halfSize < bufferSize) { + //phaseShiftStepReal = Math.cos(-Math.PI/halfSize); + //phaseShiftStepImag = Math.sin(-Math.PI/halfSize); + phaseShiftStepReal = cosTable[halfSize]; + phaseShiftStepImag = sinTable[halfSize]; + + currentPhaseShiftReal = 1; + currentPhaseShiftImag = 0; + + for (var fftStep = 0; fftStep < halfSize; fftStep++) { + i = fftStep; + + while (i < bufferSize) { + off = i + halfSize; + tr = (currentPhaseShiftReal * real[off]) - (currentPhaseShiftImag * imag[off]); + ti = (currentPhaseShiftReal * imag[off]) + (currentPhaseShiftImag * real[off]); + + real[off] = real[i] - tr; + imag[off] = imag[i] - ti; + real[i] += tr; + imag[i] += ti; + + i += halfSize << 1; + } + + tmpReal = currentPhaseShiftReal; + currentPhaseShiftReal = (tmpReal * phaseShiftStepReal) - (currentPhaseShiftImag * phaseShiftStepImag); + currentPhaseShiftImag = (tmpReal * phaseShiftStepImag) + (currentPhaseShiftImag * phaseShiftStepReal); + } + + halfSize = halfSize << 1; + } + + return this.calculateSpectrum(); +}; diff --git a/mobile/package.json b/mobile/package.json index d6befd4..4160019 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -11,7 +11,6 @@ "start": "ember server", "build": "ember cordova:build --platform=android --environment=production --release", "build-test": "ember cordova:build --platform=android", - "test": "ember test", "cordova": "ember cdv:serve --platform=android" }, "engines": { diff --git a/web/package.json b/web/package.json index 27a1c00..7e5d052 100644 --- a/web/package.json +++ b/web/package.json @@ -9,8 +9,7 @@ }, "scripts": { "start": "ember server", - "build": "ember build --env=production", - "test": "ember test" + "build": "ember build --env=production" }, "engines": { "node": ">= 0.12.0"