diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..96ef944 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,13 @@ +{ + "extends": "airbnb/legacy", + "rules": { + "func-names": 0, + "no-console": 0, + "guard-for-in": 0, + "max-len": 1 + }, + "env": { + "node": true, + "mocha": true, + } +} diff --git a/.gitignore b/.gitignore index b0e4c52..9f9d0a8 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ node_modules/ .DS_Store .project .settings/ -*.sw* +*.swp +npm-debug.log diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..971e9e6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: node_js +node_js: + - "node" +script: + - npm run lint-js + - npm test diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0b99a21 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,82 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +As of `v0.1.0`, this project adheres to [Semantic Versioning](http://semver.org/). + +## Unreleased + +## [0.2.4] - 2016-01-13 + +* Extracts macros into a standalone lib/macros.js file +* Adds Favicon (thanks @flochtililoch) +* Adds npm run test:watch action (thanks @OvisMaximus) +* Travis build will now run linter +* Updated dependencies +* Uses local version of jQuery for testing now + +## [0.2.3] - 2016-01-03 + +* Fixing bug where labels were loaded before config (thanks @flochtililoch) + +## [0.2.2] - 2016-01-01 + +* Removing `Makefile` for running tests. Only need `package.json`. +* Fixing .gitignore error for the global lirc_web build + +## [0.2.1] - 2015-12-31 + +* `lirc_web` can now be installed globally and called by `lirc_web` from CLI +* Adding ESLint to the mix and ensuring all JS conforms to Airbnb ES5 standards + +## [0.2.0] - 2015-12-30 + +* Adding `blacklist` configuration option to hide unused keys from UI (thanks @OvisMaximus) +* Adding support for SSL (thanks @de-live-gdev) +* Fixing example config in the README (thanks @de-live-gdev) +* Fixes url escaping bug with macros and remotes (issue #23) + +## [0.1.0] - 2015-12-30 + +* Locking npm versions to ensure future install work +* Adding `CHANGELOG.md` +* Adding `/refresh` link on bottom to reload UI after making changes to LIRC (thanks @f00f) +* Adding ability to set custom labels on command and remote names (thanks @elysion) +* Adding Apple mobile app capability, disabling zoom (thanks @elysion) +* Moving Lato fonts locally to remove external network dependency + +## [0.0.8] - 2014-01-18 + +* Adding `macros` configuration option +* Fixing bug with setInterval causing repeaters to potentially never stop + +## [0.0.7] - 2013-12-29 + +* Adding `send_start` and `send_stop` support to UI +* Adding `config.json` configuration file which allows users to set options +* Adding `repeaters` as a configuration file +* Adding documentation about API to README +* Setting up proper test suite with LIRC test fixtures + +## [0.0.6] - 2013-12-01 + +* Adding `upstart` example configuration files + +## [0.0.5] - 2013-08-21 + +* Locking swig dependency due to breaking change in new version +* `urlencode` command names (thanks @joe-forbes) + +## [0.0.4] - 2013-05-16 + +* Fixing iOS caching error that was preventing commands from sending + +## [0.0.3] - 2013-03-31 + +## [0.0.2] - 2013-03-22 + +* Include compiled JS and CSS for ease of installation + +## [0.0.1] - 2013-03-20 + +* Initial commit and integration with `lirc_node` diff --git a/Gruntfile.js b/Gruntfile.js index f4c12d6..08536c2 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,53 +1,60 @@ -module.exports = function(grunt) { - - // Load some tasks - grunt.loadNpmTasks('grunt-contrib-less'); - grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.loadNpmTasks('grunt-contrib-watch'); - grunt.loadNpmTasks('grunt-develop'); - - // Project configuration. - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - - uglify: { - app: { - src: ['static/js/vendor/zepto.min.js', - 'static/js/vendor/zepto.touch.js', - 'static/js/vendor/fastclick.js', - 'static/js/app/*.js'], - dest: 'static/js/compiled/app.js' - } - }, - - less: { - app: { - files: { - "static/css/compiled/app.css": "static/css/app.less" - } - } +module.exports = function (grunt) { + // Load some tasks + grunt.loadNpmTasks('grunt-contrib-less'); + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-develop'); + require('load-grunt-tasks')(grunt); // npm install --save-dev load-grunt-tasks + + // Project configuration. + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + + uglify: { + app: { + src: ['static/js/vendor/zepto.min.js', + 'static/js/vendor/zepto.touch.js', + 'static/js/vendor/fastclick.js', + 'static/js/app/*.js'], + dest: 'static/js/compiled/app.js', + }, + }, + + less: { + app: { + files: { + 'static/css/compiled/app.css': 'static/css/app.less', }, - - watch: { - scripts: { - files: ['static/js/app/*.js', 'static/js/vendor/*.js'], - tasks: ['uglify:app'] - }, - stylesheets: { - files: ['static/css/*.less'], - tasks: ['less'] - } - }, - - develop: { - server: { - file: 'app.js', - env: { NODE_ENV: 'development'} - } - } - }); - - grunt.registerTask('default', ['uglify', 'less']); - grunt.registerTask('server', ['uglify', 'less', 'develop', 'watch']); - + }, + }, + + eslint: { + target: ['Gruntfile.js', 'app.js', 'lib/**/*.js', 'test/**/*.js'], + }, + + watch: { + scripts: { + files: ['static/js/app/*.js', 'static/js/vendor/*.js'], + tasks: ['uglify:app'], + }, + stylesheets: { + files: ['static/css/*.less'], + tasks: ['less'], + }, + serverscripts: { + files: ['<%= eslint.target %>'], + tasks: ['eslint'], + }, + }, + + develop: { + server: { + file: 'app.js', + env: { NODE_ENV: 'development' }, + }, + }, + }); + + grunt.registerTask('default', ['uglify', 'less', 'eslint']); + grunt.registerTask('server', ['uglify', 'less', 'eslint', 'develop', 'watch']); }; diff --git a/Makefile b/Makefile deleted file mode 100644 index 45cb1c0..0000000 --- a/Makefile +++ /dev/null @@ -1,12 +0,0 @@ -TESTS = test/*.js -REPORTER = dot - -test: - @NODE_ENV=test ./node_modules/.bin/mocha \ - --require should \ - --require test/common.js \ - --reporter $(REPORTER) \ - --growl \ - $(TESTS) - -.PHONY: test bench diff --git a/README.md b/README.md index 3848e0e..d3aad5b 100644 --- a/README.md +++ b/README.md @@ -7,35 +7,53 @@ This project allows you to control LIRC from any web browser - phone, tablet, or This is part of the [Open Source Universal Remote](http://opensourceuniversalremote.com) project. +[![Build Status](https://travis-ci.org/alexbain/lirc_web.png)](https://travis-ci.org/alexbain/lirc_web) ## Installation -You'll need to have [LIRC](http://lirc.org) installed and configured on your machine to use ``lirc_web``. In addition, you'll need to install [nodejs](http://nodejs.org). Once you have LIRC and nodejs installed and configured, you'll be able to install ``lirc_web`` and it's dependencies: +You'll need to have [LIRC](http://lirc.org) installed and configured on your machine to use ``lirc_web``. In addition, you'll need to install [nodejs](http://nodejs.org). Once you have LIRC and nodejs installed and configured, you'll be able to install ``lirc_web``: - git clone git://github.com/alexbain/lirc_web.git - cd lirc_web - npm install - node app.js + npm install -g lirc_web + lirc_web + +Note that you may need to run the `npm install` command with `sudo`. -You're set! Verify the web interface works by opening ``http://SERVER:3000/`` in a web browser. +### Viewing -If you want to have the app available via port 80 and start on boot, there are example NGINX and Upstart configuration files included in the ``example_configs/`` directory. +Verify the web interface works by opening ``http://SERVER:3000/`` in a web browser. +If you want to have `lirc_web` available via port 80 and start on boot, there are example NGINX and Upstart configuration files included in the ``example_configs/`` directory. ## Configuration -As of v0.0.8, ``lirc_web`` supports customization through a configuration file (``config.json``) in the root of the project. There are currently four configuration options: +As of v0.0.8, ``lirc_web`` supports customization through a configuration file. + +You may place this configuration file in one of two locations and `lirc_web` will detect it: + +1. Place a file named `.lirc_web_config.json` in the home directory of the user running `lirc_web` (global installation) +2. Place a file named `config.json` in the root of the `lirc_web` project directory (local / development installation) + +These are the available configuration options: 1. ``repeaters`` - buttons that repeatedly send their commands while pressed. A common example are the volume buttons on most remote controls. While you hold the volume buttons down, the remote will repeatedly send the volume command to your device. 2. ``macros`` - a collection of commands that should be executed one after another. This allows you to automate actions like "Play Xbox 360" or "Listen to music via AirPlay". Each step in a macro is described in the format ``[ "REMOTE", "COMMAND" ]``, where ``REMOTE`` and ``COMMAND`` are defined by what you have programmed into LIRC. You can add delays between steps of macros in the format of ``[ "delay", 500 ]``. Note that the delay is measured in milliseconds so 1000 milliseconds = 1 second. 3. ``commandLabels`` - a way to rename commands that LIRC understands (``KEY_POWER``, ``KEY_VOLUMEUP``) with labels that humans prefer (``Power``, ``Volume Up``). 4. ``remoteLabels`` - a way to rename the remotes that LIRC understands (``XBOX360``) with labels that humans prefer (``Xbox 360``). +5. ``blacklists`` - a way to hide unused commands from your remotes. +6. ``server`` - server configuration settings (ports, [SSL](http://serverfault.com/a/366374)). #### Example config.json: { + "server" : { + "port" : 3000, + "ssl" : false, + "ssl_cert" : "/home/pi/lirc_web/server.cert", + "ssl_key" : "/home/pi/lirc_web/server.key", + "ssl_port" : 3001 + }, "repeaters": { "SonyTV": { "VolumeUp": true, @@ -69,12 +87,20 @@ As of v0.0.8, ``lirc_web`` supports customization through a configuration file ( "remoteLabels": { "Xbox360": "Xbox 360" } + "blacklists": { + "Yamaha": [ + "AUX2", + "AUX3" + ] + } } +Please see the `example_configs/` directory. + ## Using the JSON API -Building an app on top of lirc_web is straight forward with the included JSON based RESTful API. +Building an app on top of `lirc_web` is straight forward with the included JSON based RESTful API. API endpoints: @@ -90,9 +116,13 @@ API endpoints: ## Development Would you like to contribute to and improve ``lirc_web``? Fantastic. To contribute -patches, run tests or benchmarks, install ``lirc_web`` using the instructions above. Once that is complete, you'll need to setup the development environment. +patches, run tests or benchmarks, install ``lirc_web`` locally: + + git clone git://github.com/alexbain/lirc_web.git + cd lirc_web + npm install -Now, you'll need to setup the development environment. ``lirc_web`` uses the [GruntJS](http://gruntjs.com/) built system to make development easier. +Next, you'll need to setup the development environment. ``lirc_web`` uses the [GruntJS](http://gruntjs.com/) built system to make development easier. Install GruntJS (build environment): @@ -109,15 +139,35 @@ Install GruntJS (build environment): You can run the test suite by running: ``` -make test +npm test +``` + +If you develop test driven, you may want to launch a continuous test which automatically restarts when server or tests are modified: + ``` +npm run test:watch +``` + +You can run the linter to confirm JS conforms to standards by running: + +``` +npm run lint-js +``` + +You can also run the linter continuously via grunt: +``` +grunt watch +``` + ## Contributing Before you submit a pull request with your change, please be sure to: -* Add new tests that prove your change works as expected. -* Ensure all existing tests are still passing. +* Add new tests that prove your change works as expected +* Ensure all existing tests are still passing +* Run the linter to ensure your code conforms to the js styleguide +* Update CHANGELOG.md file ('Unreleased' section) with concise bullet points Once you're sure everything is still working, open a pull request with a clear description of what you changed and why. I will not accept a pull request which @@ -130,7 +180,7 @@ The exception to this would be refactoring existing code or changing documentati (The MIT License) -Copyright (c) 2013 Alex Bain <alex@alexba.in> +Copyright (c) 2013-2016 Alex Bain <alex@alexba.in> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/app.js b/app.js index 912aa54..e7a5582 100644 --- a/app.js +++ b/app.js @@ -1,136 +1,186 @@ -// lirc_web - v0.0.8 -// Alex Bain +#! /usr/bin/env node // Requirements -var express = require('express'), - lirc_node = require('lirc_node'), - consolidate = require('consolidate'), - path = require('path'), - swig = require('swig'), - labels = require('./lib/labels'); +var express = require('express'); +var logger = require('morgan'); +var compress = require('compression'); +var lircNode = require('lirc_node'); +var consolidate = require('consolidate'); +var swig = require('swig'); +var labels = require('./lib/labels'); +var https = require('https'); +var fs = require('fs'); +var macros = require('./lib/macros'); // Precompile templates var JST = { - index: swig.compileFile(__dirname + '/templates/index.swig') + index: swig.compileFile(__dirname + '/templates/index.swig'), }; // Create app var app = module.exports = express(); -// App configuration -app.engine('.html', consolidate.swig); -app.configure(function() { - app.use(express.logger()); - app.set('views', __dirname + '/views'); - app.set('view engine', 'jade'); - app.use(express.compress()); - app.use(express.static(__dirname + '/static')); -}); - // lirc_web configuration var config = {}; -// Based on node environment, initialize connection to lirc_node or use test data -if (process.env.NODE_ENV == 'test' || process.env.NODE_ENV == 'development') { - lirc_node.remotes = require(__dirname + '/test/fixtures/remotes.json'); - config = require(__dirname + '/test/fixtures/config.json'); -} else { - _init(); -} +// Server & SSL options +var port = 3000; +var sslOptions = { + key: null, + cert: null, +}; + +var labelFor = {}; + +// App configuration +app.engine('.html', consolidate.swig); +app.use(logger('combined')); +app.set('views', __dirname + '/views'); +app.set('view engine', 'jade'); +app.use(compress()); +app.use(express.static(__dirname + '/static')); function _init() { - lirc_node.init(); + var home = process.env.HOME; + + lircNode.init(); - // Config file is optional + // Config file is optional + try { try { - config = require(__dirname + '/config.json'); - } catch(e) { - console.log("DEBUG:", e); - console.log("WARNING: Cannot find config.json!"); + config = require(__dirname + '/config.json'); + } catch (e) { + config = require(home + '/.lirc_web_config.json'); + } + } catch (e) { + console.log('DEBUG:', e); + console.log('WARNING: Cannot find config.json!'); + } +} + +function refineRemotes(myRemotes) { + var newRemotes = {}; + var newRemoteCommands = null; + var remote = null; + + function isBlacklistExisting(remoteName) { + return config.blacklists && config.blacklists[remoteName]; + } + + function getCommandsForRemote(remoteName) { + var remoteCommands = myRemotes[remoteName]; + var blacklist = null; + + if (isBlacklistExisting(remoteName)) { + blacklist = config.blacklists[remoteName]; + + remoteCommands = remoteCommands.filter(function (command) { + return blacklist.indexOf(command) < 0; + }); } + + return remoteCommands; + } + + for (remote in myRemotes) { + newRemoteCommands = getCommandsForRemote(remote); + newRemotes[remote] = newRemoteCommands; + } + + return newRemotes; +} + +// Based on node environment, initialize connection to lircNode or use test data +if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development') { + lircNode.remotes = require(__dirname + '/test/fixtures/remotes.json'); + config = require(__dirname + '/test/fixtures/config.json'); +} else { + _init(); } +// initialize Labels for remotes / commands +labelFor = labels(config.remoteLabels, config.commandLabels); + // Routes -var labelFor = labels(config.remoteLabels, config.commandLabels) - -// Web UI -app.get('/', function(req, res) { - res.send(JST['index'].render({ - remotes: lirc_node.remotes, - macros: config.macros, - repeaters: config.repeaters, - labelForRemote: labelFor.remote, - labelForCommand: labelFor.command - })); +// Index +app.get('/', function (req, res) { + var refinedRemotes = refineRemotes(lircNode.remotes); + res.send(JST.index({ + remotes: refinedRemotes, + macros: config.macros, + repeaters: config.repeaters, + labelForRemote: labelFor.remote, + labelForCommand: labelFor.command, + })); }); // Refresh -app.get('/refresh', function(req, res) { - _init(); - res.redirect('/'); +app.get('/refresh', function (req, res) { + _init(); + res.redirect('/'); }); // List all remotes in JSON format -app.get('/remotes.json', function(req, res) { - res.json(lirc_node.remotes); +app.get('/remotes.json', function (req, res) { + res.json(refineRemotes(lircNode.remotes)); }); // List all commands for :remote in JSON format -app.get('/remotes/:remote.json', function(req, res) { - if (lirc_node.remotes[req.params.remote]) { - res.json(lirc_node.remotes[req.params.remote]); - } else { - res.send(404); - } +app.get('/remotes/:remote.json', function (req, res) { + if (lircNode.remotes[req.params.remote]) { + res.json(refineRemotes(lircNode.remotes)[req.params.remote]); + } else { + res.sendStatus(404); + } }); // List all macros in JSON format -app.get('/macros.json', function(req, res) { - res.json(config.macros); +app.get('/macros.json', function (req, res) { + res.json(config.macros); }); // List all commands for :macro in JSON format -app.get('/macros/:macro.json', function(req, res) { - if (config.macros && config.macros[req.params.macro]) { - res.json(config.macros[req.params.macro]); - } else { - res.send(404); - } +app.get('/macros/:macro.json', function (req, res) { + if (config.macros && config.macros[req.params.macro]) { + res.json(config.macros[req.params.macro]); + } else { + res.sendStatus(404); + } }); - // Send :remote/:command one time -app.post('/remotes/:remote/:command', function(req, res) { - lirc_node.irsend.send_once(req.params.remote, req.params.command, function() {}); - res.setHeader('Cache-Control', 'no-cache'); - res.send(200); +app.post('/remotes/:remote/:command', function (req, res) { + lircNode.irsend.send_once(req.params.remote, req.params.command, function () {}); + res.setHeader('Cache-Control', 'no-cache'); + res.sendStatus(200); }); // Start sending :remote/:command repeatedly -app.post('/remotes/:remote/:command/send_start', function(req, res) { - lirc_node.irsend.send_start(req.params.remote, req.params.command, function() {}); - res.setHeader('Cache-Control', 'no-cache'); - res.send(200); +app.post('/remotes/:remote/:command/send_start', function (req, res) { + lircNode.irsend.send_start(req.params.remote, req.params.command, function () {}); + res.setHeader('Cache-Control', 'no-cache'); + res.sendStatus(200); }); // Stop sending :remote/:command repeatedly -app.post('/remotes/:remote/:command/send_stop', function(req, res) { - lirc_node.irsend.send_stop(req.params.remote, req.params.command, function() {}); - res.setHeader('Cache-Control', 'no-cache'); - res.send(200); +app.post('/remotes/:remote/:command/send_stop', function (req, res) { + lircNode.irsend.send_stop(req.params.remote, req.params.command, function () {}); + res.setHeader('Cache-Control', 'no-cache'); + res.sendStatus(200); }); // Execute a macro (a collection of commands to one or more remotes) -app.post('/macros/:macro', function(req, res) { +<<<<<<< HEAD +app.post('/macros/:area/:macro', function(req, res) { // If the macro exists, execute each command in the macro with 100msec // delay between each command. - if (config.macros && config.macros[req.params.macro]) { + if (config.macros && config.macros[req.params.area]) { var i = 0; var nextCommand = function() { - var command = config.macros[req.params.macro][i]; + var command = config.macros[req.params.area][req.params.macro][i]; if (!command) { return true; } @@ -149,11 +199,38 @@ app.post('/macros/:macro', function(req, res) { nextCommand(); } +======= +app.post('/macros/:macro', function (req, res) { + // If the macro exists, execute it + if (config.macros && config.macros[req.params.macro]) { + macros.exec(config.macros[req.params.macro], lircNode); res.setHeader('Cache-Control', 'no-cache'); - res.send(200); + res.sendStatus(200); + } else { +>>>>>>> refs/remotes/alexbain/master + res.setHeader('Cache-Control', 'no-cache'); + res.sendStatus(404); + } }); +// Listen (http) +if (config.server && config.server.port) { + port = config.server.port; +} +// only start server, when called as application +if (!module.parent) { + app.listen(port); + console.log('Open Source Universal Remote UI + API has started on port ' + port + ' (http).'); +} + +// Listen (https) +if (config.server && config.server.ssl && config.server.ssl_cert && config.server.ssl_key && config.server.ssl_port) { + sslOptions = { + key: fs.readFileSync(config.server.ssl_key), + cert: fs.readFileSync(config.server.ssl_cert), + }; -// Default port is 3000 -app.listen(3000); -console.log("Open Source Universal Remote UI + API has started on port 3000."); + https.createServer(sslOptions, app).listen(config.server.ssl_port); + + console.log('Open Source Universal Remote UI + API has started on port ' + config.server.ssl_port + ' (https).'); +} diff --git a/example_configs/config.json b/example_configs/config.json index dd6436e..22b108e 100644 --- a/example_configs/config.json +++ b/example_configs/config.json @@ -1,4 +1,11 @@ { + "server" : { + "port" : 3000, + "ssl" : false, + "ssl_cert" : "/home/pi/lirc_web/server.cert", + "ssl_key" : "/home/pi/lirc_web/server.key", + "ssl_port" : 3001 + }, "repeaters": { "SonyTV": { "VolumeUp": true, @@ -7,11 +14,11 @@ }, "macros": { "Xbox360": [ - { "SonyTV": "Power" }, - { "SonyTV": "Xbox360" }, - { "Yamaha": "Power" }, - { "Yamaha": "Xbox360" }, - { "Xbox360": "Power" } + [ "SonyTV", "Power" ], + [ "SonyTV", "Xbox360" ], + [ "Yamaha", "Power" ], + [ "Yamaha", "Xbox360" ], + [ "Xbox360", "Power" ] ] }, "commandLabels": { @@ -23,5 +30,11 @@ }, "remoteLabels": { "Xbox360": "Xbox 360" + }, + "blacklists": { + "Yamaha": [ + "AUX2", + "AUX3" + ] } } diff --git a/example_configs/upstart/standalone_upstart.conf b/example_configs/upstart/standalone_upstart.conf new file mode 100644 index 0000000..20263d8 --- /dev/null +++ b/example_configs/upstart/standalone_upstart.conf @@ -0,0 +1,37 @@ +# /etc/init/open-source-universal-remote.conf +description "universalremote.local" + +start on runlevel [2345] +stop on runlevel [016] + +# Restart when job dies +respawn + +# Give up restart after 5 respawns in 60 seconds +respawn limit 5 60 + +script + + # Store the pid file in /var/run + echo $$ > /var/run/open-source-universal-remote.pid + + # Should run in 'production' mode + env NODE_ENV=production + + # File to execute, where to pipe logs + exec lirc_web 2>&1 >> /var/log/open-source-universal-remote.upstart.log + +end script + +pre-start script + # Log that lirc_web started up + echo "[`date -u +%Y-%m-%dT%T.%3NZ`] (sys) Starting" >> /var/log/open-source-universal-remote.upstart.log +end script + +pre-stop script + # Remove pid file + rm /var/run/open-source-universal-remote.pid + + # Log that lirc_web shut down + echo "[`date -u +%Y-%m-%dT%T.%3NZ`] (sys) Stopping" >> /var/log/open-source-universal-remote.upstart.log +end script diff --git a/lib/labels.js b/lib/labels.js index 19d39e9..ae5efcf 100644 --- a/lib/labels.js +++ b/lib/labels.js @@ -1,9 +1,4 @@ -module.exports = function(remoteLabels, commandLabels) { - return { - command: getCommandLabel, - remote: getRemoteLabel - }; - +module.exports = function Labels(remoteLabels, commandLabels) { function getCommandLabel(remote, command) { return commandLabels && commandLabels[remote] && commandLabels[remote][command] ? commandLabels[remote][command] : command; } @@ -11,4 +6,9 @@ module.exports = function(remoteLabels, commandLabels) { function getRemoteLabel(remote) { return remoteLabels && remoteLabels[remote] ? remoteLabels[remote] : remote; } -} + + return { + command: getCommandLabel, + remote: getRemoteLabel, + }; +}; diff --git a/lib/macros.js b/lib/macros.js new file mode 100644 index 0000000..5fd977a --- /dev/null +++ b/lib/macros.js @@ -0,0 +1,28 @@ +function exec(macro, lircNode, iter) { + var i = iter || 0; + + // select the command from the sequence + var command = macro[i]; + + if (!command) { return false; } + + i = i + 1; + + // if the command is delay, wait N msec and then execute next command + if (command[0] === 'delay') { + setTimeout(function () { + exec(macro, lircNode, i); + }, command[1]); + } else { + // By default, wait 100msec before calling next command + lircNode.irsend.send_once(command[0], command[1], function () { + setTimeout(function () { + exec(macro, lircNode, i); + }, 100); + }); + } + + return true; +} + +exports.exec = exec; diff --git a/package.json b/package.json index 99e8493..7182ebb 100644 --- a/package.json +++ b/package.json @@ -2,29 +2,43 @@ "name": "lirc_web", "description": "A NodeJS / Express app that creates a web UI + API for LIRC", "main": "app.js", - "version": "0.0.8", - "private": true, + "version": "0.2.4", + "preferGlobal": true, "dependencies": { - "express": "3.0.6", - "lirc_node": "*", - "swig": "0.13.5", - "consolidate": "0.11.0" + "compression": "^1.6.0", + "consolidate": "^0.13.1", + "express": "^4.13.0", + "lirc_node": "0.0.4", + "lodash": "^3.10.1", + "morgan": "^1.6.1", + "swig": "^1.4.2" }, "devDependencies": { - "mocha": "*", - "should": "7.1.0", - "sinon": "*", - "supertest": "1.1.0", - "nodemon": "*", + "eslint": "^1.10.0", + "eslint-config-airbnb": "^3.1.0", + "eslint-plugin-react": "^3.15.0", "grunt": "0.4.5", - "grunt-contrib-less": "1.0.1", - "grunt-contrib-uglify": "0.9.2", + "grunt-contrib-less": "^1.1.0", + "grunt-contrib-uglify": "^0.11.0", "grunt-contrib-watch": "0.6.1", - "jsdom": "*", - "grunt-develop": "0.3.0" + "grunt-develop": "^0.4.0", + "grunt-eslint": "17.3.1", + "jquery": "^2.2.0", + "jsdom": "^7.2.2", + "load-grunt-tasks": "^3.4.0", + "mocha": "2.3.4", + "nodemon": "1.8.1", + "should": "^8.1.1", + "sinon": "1.17.2", + "supertest": "1.1.0" + }, + "bin": { + "lirc_web": "./app.js" }, "scripts": { - "test": "make test" + "test": "NODE_ENV=test mocha --require should --require test/common.js --reporter dot test/**", + "test:watch": "NODE_ENV=test mocha --require should --require test/common.js --watch --reporter dot lib/** test/** ", + "lint-js": "eslint app.js lib/** test/**" }, "repository": { "type": "git", diff --git a/static/css/app.less b/static/css/app.less index 0c541bf..ce284a4 100644 --- a/static/css/app.less +++ b/static/css/app.less @@ -28,9 +28,15 @@ ul { padding: 0; } +.macrocommands li { + margin: 20px 0; + padding: 0; +} .commands li { margin: 20px 0; padding: 0; + width: 120px; + display: inline-block; } h1 { @@ -52,6 +58,25 @@ h1 { } } +h2 { + background: @colorWetAsphalt; + color: #fff; + font-family: LatoBold; + margin: 0 0 30px; + padding: 10px 0; + text-align: center; + z-index: 6; + + left: 5px; + position: fixed; + top: 0; +// width: 20%; + + &.is-remote { + background: @colorPeterRiver; + } +} + .command { margin: 20px 0; } @@ -82,12 +107,22 @@ h1 { width: 100%; } +.macro { + margin: 0; + padding: 0; + position: relative; +} + .remote { margin: 0; padding: 0; position: relative; } +.remotes-navmenu, +.macros-navmenu { +} + .remotes-nav, .macros-nav { } @@ -98,9 +133,9 @@ h1 { } .back { - position: absolute; + position: fixed; top: 7px; - left: 8px; + left: 30px; cursor: pointer; } @@ -121,6 +156,14 @@ a:active, margin: 0 40px; } +.macro { + display: none; +} + +.macro.active { + display: block; +} + .remote { display: none; } @@ -141,7 +184,7 @@ footer { padding: 10px 0; } -footer a { +footer p, footer a { font-size: 12px; font-weight: 300; } diff --git a/static/css/compiled/app.css b/static/css/compiled/app.css index f6aedae..a6fc13e 100644 --- a/static/css/compiled/app.css +++ b/static/css/compiled/app.css @@ -16,9 +16,15 @@ ul { margin: 0; padding: 0; } +.macrocommands li { + margin: 20px 0; + padding: 0; +} .commands li { margin: 20px 0; padding: 0; + width: 120px; + display: inline-block; } h1 { background: #34495e; @@ -36,6 +42,21 @@ h1 { h1.is-remote { background: #3498db; } +h2 { + background: #34495e; + color: #fff; + font-family: LatoBold; + margin: 0 0 30px; + padding: 10px 0; + text-align: center; + z-index: 6; + left: 5px; + position: fixed; + top: 0; +} +h2.is-remote { + background: #3498db; +} .command { margin: 20px 0; } @@ -59,6 +80,11 @@ h1.is-remote { padding-right: 0; width: 100%; } +.macro { + margin: 0; + padding: 0; + position: relative; +} .remote { margin: 0; padding: 0; @@ -69,9 +95,9 @@ h1.is-remote { margin: 20px 0; } .back { - position: absolute; + position: fixed; top: 7px; - left: 8px; + left: 30px; cursor: pointer; } a, @@ -89,6 +115,12 @@ a:active, #container { margin: 0 40px; } +.macro { + display: none; +} +.macro.active { + display: block; +} .remote { display: none; } @@ -105,6 +137,7 @@ footer { width: 100%; padding: 10px 0; } +footer p, footer a { font-size: 12px; font-weight: 300; diff --git a/static/css/pushy.css b/static/css/pushy.css new file mode 100644 index 0000000..53813f8 --- /dev/null +++ b/static/css/pushy.css @@ -0,0 +1,120 @@ +/*! Pushy - v0.9.2 - 2014-9-13 +* Pushy is a responsive off-canvas navigation menu using CSS transforms & transitions. +* https://github.com/christophery/pushy/ +* by Christopher Yee */ + +/* Menu Appearance */ + +.pushy{ + position: fixed; + width: 200px; + height: 100%; + top: 0; + z-index: 9999; + background: #333332; + font-size: 0.9em; + font-weight: bold; + -webkit-box-shadow: inset -10px 0 6px -9px rgba(0, 0, 0, .7); + -moz-box-shadow: inset -10px 0 6px -9px rgba(0, 0, 0, .7); + box-shadow: inset -10px 0 6px -9px rgba(0, 0, 0, .7); + overflow: auto; + -webkit-overflow-scrolling: touch; /* enables momentum scrolling in iOS overflow elements */ +} + +.pushy a{ + display: block; + color: #b3b3b1; + padding: 15px 30px; + border-bottom: 1px solid rgba(0, 0, 0, .1); + border-top: 1px solid rgba(255, 255, 255, .1); + text-decoration: none; +} + +.pushy a:hover{ + background: #00b4ff; + color: #FFF; +} + +/* Menu Movement */ + +.pushy-left{ + -webkit-transform: translate3d(-200px,0,0); + -moz-transform: translate3d(-200px,0,0); + -ms-transform: translate3d(-200px,0,0); + -o-transform: translate3d(-200px,0,0); + transform: translate3d(-200px,0,0); +} + +.pushy-open{ + -webkit-transform: translate3d(0,0,0); + -moz-transform: translate3d(0,0,0); + -ms-transform: translate3d(0,0,0); + -o-transform: translate3d(0,0,0); + transform: translate3d(0,0,0); +} + +.container-push, .push-push{ + -webkit-transform: translate3d(200px,0,0); + -moz-transform: translate3d(200px,0,0); + -ms-transform: translate3d(200px,0,0); + -o-transform: translate3d(200px,0,0); + transform: translate3d(200px,0,0); +} + +/* Menu Transitions */ + +.pushy, #container, .push{ + -webkit-transition: -webkit-transform .2s cubic-bezier(.16, .68, .43, .99); + -moz-transition: -moz-transform .2s cubic-bezier(.16, .68, .43, .99); + -o-transition: -o-transform .2s cubic-bezier(.16, .68, .43, .99); + transition: transform .2s cubic-bezier(.16, .68, .43, .99); +} + +/* Site Overlay */ + +.site-overlay{ + display: none; +} + +.pushy-active .site-overlay{ + display: block; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 9998; + background-color: rgba(0,0,0,0.5); + -webkit-animation: fade 500ms; + -moz-animation: fade 500ms; + -o-animation: fade 500ms; + animation: fade 500ms; +} + +@keyframes fade{ + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +@-moz-keyframes fade{ + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +@-webkit-keyframes fade{ + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +@-o-keyframes fade{ + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +/* Example Media Query */ + +@media screen and (max-width: 768px){ + .pushy{ + font-size: 1.0em; + } +} diff --git a/static/favicons/android-chrome-144x144.png b/static/favicons/android-chrome-144x144.png new file mode 100644 index 0000000..137dd2f Binary files /dev/null and b/static/favicons/android-chrome-144x144.png differ diff --git a/static/favicons/android-chrome-192x192.png b/static/favicons/android-chrome-192x192.png new file mode 100644 index 0000000..e5efc49 Binary files /dev/null and b/static/favicons/android-chrome-192x192.png differ diff --git a/static/favicons/android-chrome-36x36.png b/static/favicons/android-chrome-36x36.png new file mode 100644 index 0000000..75a7d4b Binary files /dev/null and b/static/favicons/android-chrome-36x36.png differ diff --git a/static/favicons/android-chrome-48x48.png b/static/favicons/android-chrome-48x48.png new file mode 100644 index 0000000..2eea92e Binary files /dev/null and b/static/favicons/android-chrome-48x48.png differ diff --git a/static/favicons/android-chrome-72x72.png b/static/favicons/android-chrome-72x72.png new file mode 100644 index 0000000..57920ba Binary files /dev/null and b/static/favicons/android-chrome-72x72.png differ diff --git a/static/favicons/android-chrome-96x96.png b/static/favicons/android-chrome-96x96.png new file mode 100644 index 0000000..b88f7fc Binary files /dev/null and b/static/favicons/android-chrome-96x96.png differ diff --git a/static/favicons/apple-touch-icon-114x114.png b/static/favicons/apple-touch-icon-114x114.png new file mode 100644 index 0000000..af1b3d0 Binary files /dev/null and b/static/favicons/apple-touch-icon-114x114.png differ diff --git a/static/favicons/apple-touch-icon-120x120.png b/static/favicons/apple-touch-icon-120x120.png new file mode 100644 index 0000000..42adb7a Binary files /dev/null and b/static/favicons/apple-touch-icon-120x120.png differ diff --git a/static/favicons/apple-touch-icon-144x144.png b/static/favicons/apple-touch-icon-144x144.png new file mode 100644 index 0000000..7ef17dd Binary files /dev/null and b/static/favicons/apple-touch-icon-144x144.png differ diff --git a/static/favicons/apple-touch-icon-152x152.png b/static/favicons/apple-touch-icon-152x152.png new file mode 100644 index 0000000..29df5b4 Binary files /dev/null and b/static/favicons/apple-touch-icon-152x152.png differ diff --git a/static/favicons/apple-touch-icon-180x180.png b/static/favicons/apple-touch-icon-180x180.png new file mode 100644 index 0000000..b6e9128 Binary files /dev/null and b/static/favicons/apple-touch-icon-180x180.png differ diff --git a/static/favicons/apple-touch-icon-57x57.png b/static/favicons/apple-touch-icon-57x57.png new file mode 100644 index 0000000..693888f Binary files /dev/null and b/static/favicons/apple-touch-icon-57x57.png differ diff --git a/static/favicons/apple-touch-icon-60x60.png b/static/favicons/apple-touch-icon-60x60.png new file mode 100644 index 0000000..43d8e4c Binary files /dev/null and b/static/favicons/apple-touch-icon-60x60.png differ diff --git a/static/favicons/apple-touch-icon-72x72.png b/static/favicons/apple-touch-icon-72x72.png new file mode 100644 index 0000000..fa98a54 Binary files /dev/null and b/static/favicons/apple-touch-icon-72x72.png differ diff --git a/static/favicons/apple-touch-icon-76x76.png b/static/favicons/apple-touch-icon-76x76.png new file mode 100644 index 0000000..9a13ccc Binary files /dev/null and b/static/favicons/apple-touch-icon-76x76.png differ diff --git a/static/favicons/apple-touch-icon-precomposed.png b/static/favicons/apple-touch-icon-precomposed.png new file mode 100644 index 0000000..7985ff9 Binary files /dev/null and b/static/favicons/apple-touch-icon-precomposed.png differ diff --git a/static/favicons/apple-touch-icon.png b/static/favicons/apple-touch-icon.png new file mode 100644 index 0000000..b6e9128 Binary files /dev/null and b/static/favicons/apple-touch-icon.png differ diff --git a/static/favicons/browserconfig.xml b/static/favicons/browserconfig.xml new file mode 100644 index 0000000..32c5c42 --- /dev/null +++ b/static/favicons/browserconfig.xml @@ -0,0 +1,12 @@ + + + + + + + + + #ffc40d + + + diff --git a/static/favicons/favicon-16x16.png b/static/favicons/favicon-16x16.png new file mode 100644 index 0000000..0203554 Binary files /dev/null and b/static/favicons/favicon-16x16.png differ diff --git a/static/favicons/favicon-194x194.png b/static/favicons/favicon-194x194.png new file mode 100644 index 0000000..8ee8f56 Binary files /dev/null and b/static/favicons/favicon-194x194.png differ diff --git a/static/favicons/favicon-32x32.png b/static/favicons/favicon-32x32.png new file mode 100644 index 0000000..81402b6 Binary files /dev/null and b/static/favicons/favicon-32x32.png differ diff --git a/static/favicons/favicon-96x96.png b/static/favicons/favicon-96x96.png new file mode 100644 index 0000000..f9f38b7 Binary files /dev/null and b/static/favicons/favicon-96x96.png differ diff --git a/static/favicons/favicon.ico b/static/favicons/favicon.ico new file mode 100644 index 0000000..2ec8e63 Binary files /dev/null and b/static/favicons/favicon.ico differ diff --git a/static/favicons/manifest.json b/static/favicons/manifest.json new file mode 100644 index 0000000..76e3763 --- /dev/null +++ b/static/favicons/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "Universal Remote", + "icons": [ + { + "src": "\/favicons\/android-chrome-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": 0.75 + }, + { + "src": "\/favicons\/android-chrome-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": 1 + }, + { + "src": "\/favicons\/android-chrome-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": 1.5 + }, + { + "src": "\/favicons\/android-chrome-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": 2 + }, + { + "src": "\/favicons\/android-chrome-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": 3 + }, + { + "src": "\/favicons\/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": 4 + } + ] +} diff --git a/static/favicons/mstile-144x144.png b/static/favicons/mstile-144x144.png new file mode 100644 index 0000000..9159d2e Binary files /dev/null and b/static/favicons/mstile-144x144.png differ diff --git a/static/favicons/mstile-150x150.png b/static/favicons/mstile-150x150.png new file mode 100644 index 0000000..fd99adf Binary files /dev/null and b/static/favicons/mstile-150x150.png differ diff --git a/static/favicons/mstile-310x150.png b/static/favicons/mstile-310x150.png new file mode 100644 index 0000000..15eb069 Binary files /dev/null and b/static/favicons/mstile-310x150.png differ diff --git a/static/favicons/mstile-310x310.png b/static/favicons/mstile-310x310.png new file mode 100644 index 0000000..bcb2f2b Binary files /dev/null and b/static/favicons/mstile-310x310.png differ diff --git a/static/favicons/mstile-70x70.png b/static/favicons/mstile-70x70.png new file mode 100644 index 0000000..aaccb13 Binary files /dev/null and b/static/favicons/mstile-70x70.png differ diff --git a/static/favicons/safari-pinned-tab.svg b/static/favicons/safari-pinned-tab.svg new file mode 100644 index 0000000..a0023f4 --- /dev/null +++ b/static/favicons/safari-pinned-tab.svg @@ -0,0 +1,35 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + + + diff --git a/static/js/compiled/app.js b/static/js/compiled/app.js index d312aff..72a3c18 100644 --- a/static/js/compiled/app.js +++ b/static/js/compiled/app.js @@ -1,2 +1,2 @@ function FastClick(a){"use strict";var b,c=this;if(this.trackingClick=!1,this.trackingClickStart=0,this.targetElement=null,this.touchStartX=0,this.touchStartY=0,this.lastTouchIdentifier=0,this.layer=a,!a||!a.nodeType)throw new TypeError("Layer must be a document node");this.onClick=function(){return FastClick.prototype.onClick.apply(c,arguments)},this.onMouse=function(){return FastClick.prototype.onMouse.apply(c,arguments)},this.onTouchStart=function(){return FastClick.prototype.onTouchStart.apply(c,arguments)},this.onTouchEnd=function(){return FastClick.prototype.onTouchEnd.apply(c,arguments)},this.onTouchCancel=function(){return FastClick.prototype.onTouchCancel.apply(c,arguments)},"undefined"!=typeof window.ontouchstart&&(this.deviceIsAndroid&&(a.addEventListener("mouseover",this.onMouse,!0),a.addEventListener("mousedown",this.onMouse,!0),a.addEventListener("mouseup",this.onMouse,!0)),a.addEventListener("click",this.onClick,!0),a.addEventListener("touchstart",this.onTouchStart,!1),a.addEventListener("touchend",this.onTouchEnd,!1),a.addEventListener("touchcancel",this.onTouchCancel,!1),Event.prototype.stopImmediatePropagation||(a.removeEventListener=function(b,c,d){var e=Node.prototype.removeEventListener;"click"===b?e.call(a,b,c.hijacked||c,d):e.call(a,b,c,d)},a.addEventListener=function(b,c,d){var e=Node.prototype.addEventListener;"click"===b?e.call(a,b,c.hijacked||(c.hijacked=function(a){a.propagationStopped||c(a)}),d):e.call(a,b,c,d)}),"function"==typeof a.onclick&&(b=a.onclick,a.addEventListener("click",function(a){b(a)},!1),a.onclick=null))}!function(a){String.prototype.trim===a&&(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")}),Array.prototype.reduce===a&&(Array.prototype.reduce=function(b){if(void 0===this||null===this)throw new TypeError;var c,d=Object(this),e=d.length>>>0,f=0;if("function"!=typeof b)throw new TypeError;if(0==e&&1==arguments.length)throw new TypeError;if(arguments.length>=2)c=arguments[1];else for(;;){if(f in d){c=d[f++];break}if(++f>=e)throw new TypeError}for(;e>f;)f in d&&(c=b.call(a,c,d[f],f,d)),f++;return c})}();var Zepto=function(){function a(a){return null==a?String(a):W[X.call(a)]||"object"}function b(b){return"function"==a(b)}function c(a){return null!=a&&a==a.window}function d(a){return null!=a&&a.nodeType==a.DOCUMENT_NODE}function e(b){return"object"==a(b)}function f(a){return e(a)&&!c(a)&&a.__proto__==Object.prototype}function g(a){return a instanceof Array}function h(a){return"number"==typeof a.length}function i(a){return E.call(a,function(a){return null!=a})}function j(a){return a.length>0?y.fn.concat.apply([],a):a}function k(a){return a.replace(/::/g,"/").replace(/([A-Z]+)([A-Z][a-z])/g,"$1_$2").replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/_/g,"-").toLowerCase()}function l(a){return a in H?H[a]:H[a]=new RegExp("(^|\\s)"+a+"(\\s|$)")}function m(a,b){return"number"!=typeof b||J[k(a)]?b:b+"px"}function n(a){var b,c;return G[a]||(b=F.createElement(a),F.body.appendChild(b),c=I(b,"").getPropertyValue("display"),b.parentNode.removeChild(b),"none"==c&&(c="block"),G[a]=c),G[a]}function o(a){return"children"in a?D.call(a.children):y.map(a.childNodes,function(a){return 1==a.nodeType?a:void 0})}function p(a,b,c){for(x in b)c&&(f(b[x])||g(b[x]))?(f(b[x])&&!f(a[x])&&(a[x]={}),g(b[x])&&!g(a[x])&&(a[x]=[]),p(a[x],b[x],c)):b[x]!==w&&(a[x]=b[x])}function q(a,b){return b===w?y(a):y(a).filter(b)}function r(a,c,d,e){return b(c)?c.call(a,d,e):c}function s(a,b,c){null==c?a.removeAttribute(b):a.setAttribute(b,c)}function t(a,b){var c=a.className,d=c&&c.baseVal!==w;return b===w?d?c.baseVal:c:void(d?c.baseVal=b:a.className=b)}function u(a){var b;try{return a?"true"==a||("false"==a?!1:"null"==a?null:isNaN(b=Number(a))?/^[\[\{]/.test(a)?y.parseJSON(a):a:b):a}catch(c){return a}}function v(a,b){b(a);for(var c in a.childNodes)v(a.childNodes[c],b)}var w,x,y,z,A,B,C=[],D=C.slice,E=C.filter,F=window.document,G={},H={},I=F.defaultView.getComputedStyle,J={"column-count":1,columns:1,"font-weight":1,"line-height":1,opacity:1,"z-index":1,zoom:1},K=/^\s*<(\w+|!)[^>]*>/,L=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,M=/^(?:body|html)$/i,N=["val","css","html","text","data","width","height","offset"],O=["after","prepend","before","append"],P=F.createElement("table"),Q=F.createElement("tr"),R={tr:F.createElement("tbody"),tbody:P,thead:P,tfoot:P,td:Q,th:Q,"*":F.createElement("div")},S=/complete|loaded|interactive/,T=/^\.([\w-]+)$/,U=/^#([\w-]*)$/,V=/^[\w-]+$/,W={},X=W.toString,Y={},Z=F.createElement("div");return Y.matches=function(a,b){if(!a||1!==a.nodeType)return!1;var c=a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.matchesSelector;if(c)return c.call(a,b);var d,e=a.parentNode,f=!e;return f&&(e=Z).appendChild(a),d=~Y.qsa(e,b).indexOf(a),f&&Z.removeChild(a),d},A=function(a){return a.replace(/-+(.)?/g,function(a,b){return b?b.toUpperCase():""})},B=function(a){return E.call(a,function(b,c){return a.indexOf(b)==c})},Y.fragment=function(a,b,c){a.replace&&(a=a.replace(L,"<$1>")),b===w&&(b=K.test(a)&&RegExp.$1),b in R||(b="*");var d,e,g=R[b];return g.innerHTML=""+a,e=y.each(D.call(g.childNodes),function(){g.removeChild(this)}),f(c)&&(d=y(e),y.each(c,function(a,b){N.indexOf(a)>-1?d[a](b):d.attr(a,b)})),e},Y.Z=function(a,b){return a=a||[],a.__proto__=y.fn,a.selector=b||"",a},Y.isZ=function(a){return a instanceof Y.Z},Y.init=function(a,c){if(!a)return Y.Z();if(b(a))return y(F).ready(a);if(Y.isZ(a))return a;var d;if(g(a))d=i(a);else if(e(a))d=[f(a)?y.extend({},a):a],a=null;else if(K.test(a))d=Y.fragment(a.trim(),RegExp.$1,c),a=null;else{if(c!==w)return y(c).find(a);d=Y.qsa(F,a)}return Y.Z(d,a)},y=function(a,b){return Y.init(a,b)},y.extend=function(a){var b,c=D.call(arguments,1);return"boolean"==typeof a&&(b=a,a=c.shift()),c.forEach(function(c){p(a,c,b)}),a},Y.qsa=function(a,b){var c;return d(a)&&U.test(b)?(c=a.getElementById(RegExp.$1))?[c]:[]:1!==a.nodeType&&9!==a.nodeType?[]:D.call(T.test(b)?a.getElementsByClassName(RegExp.$1):V.test(b)?a.getElementsByTagName(b):a.querySelectorAll(b))},y.contains=function(a,b){return a!==b&&a.contains(b)},y.type=a,y.isFunction=b,y.isWindow=c,y.isArray=g,y.isPlainObject=f,y.isEmptyObject=function(a){var b;for(b in a)return!1;return!0},y.inArray=function(a,b,c){return C.indexOf.call(b,a,c)},y.camelCase=A,y.trim=function(a){return a.trim()},y.uuid=0,y.support={},y.expr={},y.map=function(a,b){var c,d,e,f=[];if(h(a))for(d=0;d=0?a:a+this.length]},toArray:function(){return this.get()},size:function(){return this.length},remove:function(){return this.each(function(){null!=this.parentNode&&this.parentNode.removeChild(this)})},each:function(a){return C.every.call(this,function(b,c){return a.call(b,c,b)!==!1}),this},filter:function(a){return b(a)?this.not(this.not(a)):y(E.call(this,function(b){return Y.matches(b,a)}))},add:function(a,b){return y(B(this.concat(y(a,b))))},is:function(a){return this.length>0&&Y.matches(this[0],a)},not:function(a){var c=[];if(b(a)&&a.call!==w)this.each(function(b){a.call(this,b)||c.push(this)});else{var d="string"==typeof a?this.filter(a):h(a)&&b(a.item)?D.call(a):y(a);this.forEach(function(a){d.indexOf(a)<0&&c.push(a)})}return y(c)},has:function(a){return this.filter(function(){return e(a)?y.contains(this,a):y(this).find(a).size()})},eq:function(a){return-1===a?this.slice(a):this.slice(a,+a+1)},first:function(){var a=this[0];return a&&!e(a)?a:y(a)},last:function(){var a=this[this.length-1];return a&&!e(a)?a:y(a)},find:function(a){var b,c=this;return b="object"==typeof a?y(a).filter(function(){var a=this;return C.some.call(c,function(b){return y.contains(b,a)})}):1==this.length?y(Y.qsa(this[0],a)):this.map(function(){return Y.qsa(this,a)})},closest:function(a,b){var c=this[0],e=!1;for("object"==typeof a&&(e=y(a));c&&!(e?e.indexOf(c)>=0:Y.matches(c,a));)c=c!==b&&!d(c)&&c.parentNode;return y(c)},parents:function(a){for(var b=[],c=this;c.length>0;)c=y.map(c,function(a){return(a=a.parentNode)&&!d(a)&&b.indexOf(a)<0?(b.push(a),a):void 0});return q(b,a)},parent:function(a){return q(B(this.pluck("parentNode")),a)},children:function(a){return q(this.map(function(){return o(this)}),a)},contents:function(){return this.map(function(){return D.call(this.childNodes)})},siblings:function(a){return q(this.map(function(a,b){return E.call(o(b.parentNode),function(a){return a!==b})}),a)},empty:function(){return this.each(function(){this.innerHTML=""})},pluck:function(a){return y.map(this,function(b){return b[a]})},show:function(){return this.each(function(){"none"==this.style.display&&(this.style.display=null),"none"==I(this,"").getPropertyValue("display")&&(this.style.display=n(this.nodeName))})},replaceWith:function(a){return this.before(a).remove()},wrap:function(a){var c=b(a);if(this[0]&&!c)var d=y(a).get(0),e=d.parentNode||this.length>1;return this.each(function(b){y(this).wrapAll(c?a.call(this,b):e?d.cloneNode(!0):d)})},wrapAll:function(a){if(this[0]){y(this[0]).before(a=y(a));for(var b;(b=a.children()).length;)a=b.first();y(a).append(this)}return this},wrapInner:function(a){var c=b(a);return this.each(function(b){var d=y(this),e=d.contents(),f=c?a.call(this,b):a;e.length?e.wrapAll(f):d.append(f)})},unwrap:function(){return this.parent().each(function(){y(this).replaceWith(y(this).children())}),this},clone:function(){return this.map(function(){return this.cloneNode(!0)})},hide:function(){return this.css("display","none")},toggle:function(a){return this.each(function(){var b=y(this);(a===w?"none"==b.css("display"):a)?b.show():b.hide()})},prev:function(a){return y(this.pluck("previousElementSibling")).filter(a||"*")},next:function(a){return y(this.pluck("nextElementSibling")).filter(a||"*")},html:function(a){return a===w?this.length>0?this[0].innerHTML:null:this.each(function(b){var c=this.innerHTML;y(this).empty().append(r(this,a,b,c))})},text:function(a){return a===w?this.length>0?this[0].textContent:null:this.each(function(){this.textContent=a})},attr:function(a,b){var c;return"string"==typeof a&&b===w?0==this.length||1!==this[0].nodeType?w:"value"==a&&"INPUT"==this[0].nodeName?this.val():!(c=this[0].getAttribute(a))&&a in this[0]?this[0][a]:c:this.each(function(c){if(1===this.nodeType)if(e(a))for(x in a)s(this,x,a[x]);else s(this,a,r(this,b,c,this.getAttribute(a)))})},removeAttr:function(a){return this.each(function(){1===this.nodeType&&s(this,a)})},prop:function(a,b){return b===w?this[0]&&this[0][a]:this.each(function(c){this[a]=r(this,b,c,this[a])})},data:function(a,b){var c=this.attr("data-"+k(a),b);return null!==c?u(c):w},val:function(a){return a===w?this[0]&&(this[0].multiple?y(this[0]).find("option").filter(function(a){return this.selected}).pluck("value"):this[0].value):this.each(function(b){this.value=r(this,a,b,this.value)})},offset:function(a){if(a)return this.each(function(b){var c=y(this),d=r(this,a,b,c.offset()),e=c.offsetParent().offset(),f={top:d.top-e.top,left:d.left-e.left};"static"==c.css("position")&&(f.position="relative"),c.css(f)});if(0==this.length)return null;var b=this[0].getBoundingClientRect();return{left:b.left+window.pageXOffset,top:b.top+window.pageYOffset,width:Math.round(b.width),height:Math.round(b.height)}},css:function(b,c){if(arguments.length<2&&"string"==typeof b)return this[0]&&(this[0].style[A(b)]||I(this[0],"").getPropertyValue(b));var d="";if("string"==a(b))c||0===c?d=k(b)+":"+m(b,c):this.each(function(){this.style.removeProperty(k(b))});else for(x in b)b[x]||0===b[x]?d+=k(x)+":"+m(x,b[x])+";":this.each(function(){this.style.removeProperty(k(x))});return this.each(function(){this.style.cssText+=";"+d})},index:function(a){return a?this.indexOf(y(a)[0]):this.parent().children().indexOf(this[0])},hasClass:function(a){return C.some.call(this,function(a){return this.test(t(a))},l(a))},addClass:function(a){return this.each(function(b){z=[];var c=t(this),d=r(this,a,b,c);d.split(/\s+/g).forEach(function(a){y(this).hasClass(a)||z.push(a)},this),z.length&&t(this,c+(c?" ":"")+z.join(" "))})},removeClass:function(a){return this.each(function(b){return a===w?t(this,""):(z=t(this),r(this,a,b,z).split(/\s+/g).forEach(function(a){z=z.replace(l(a)," ")}),t(this,z.trim()),void 0)})},toggleClass:function(a,b){return this.each(function(c){var d=y(this),e=r(this,a,c,t(this));e.split(/\s+/g).forEach(function(a){(b===w?!d.hasClass(a):b)?d.addClass(a):d.removeClass(a)})})},scrollTop:function(){return this.length?"scrollTop"in this[0]?this[0].scrollTop:this[0].scrollY:void 0},position:function(){if(this.length){var a=this[0],b=this.offsetParent(),c=this.offset(),d=M.test(b[0].nodeName)?{top:0,left:0}:b.offset();return c.top-=parseFloat(y(a).css("margin-top"))||0,c.left-=parseFloat(y(a).css("margin-left"))||0,d.top+=parseFloat(y(b[0]).css("border-top-width"))||0,d.left+=parseFloat(y(b[0]).css("border-left-width"))||0,{top:c.top-d.top,left:c.left-d.left}}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||F.body;a&&!M.test(a.nodeName)&&"static"==y(a).css("position");)a=a.offsetParent;return a})}},y.fn.detach=y.fn.remove,["width","height"].forEach(function(a){y.fn[a]=function(b){var e,f=this[0],g=a.replace(/./,function(a){return a[0].toUpperCase()});return b===w?c(f)?f["inner"+g]:d(f)?f.documentElement["offset"+g]:(e=this.offset())&&e[a]:this.each(function(c){f=y(this),f.css(a,r(this,b,c,f[a]()))})}}),O.forEach(function(b,c){var d=c%2;y.fn[b]=function(){var b,e,f=y.map(arguments,function(c){return b=a(c),"object"==b||"array"==b||null==c?c:Y.fragment(c)}),g=this.length>1;return f.length<1?this:this.each(function(a,b){e=d?b:b.parentNode,b=0==c?b.nextSibling:1==c?b.firstChild:2==c?b:null,f.forEach(function(a){if(g)a=a.cloneNode(!0);else if(!e)return y(a).remove();v(e.insertBefore(a,b),function(a){null!=a.nodeName&&"SCRIPT"===a.nodeName.toUpperCase()&&(!a.type||"text/javascript"===a.type)&&!a.src&&window.eval.call(window,a.innerHTML)})})})},y.fn[d?b+"To":"insert"+(c?"Before":"After")]=function(a){return y(a)[b](this),this}}),Y.Z.prototype=y.fn,Y.uniq=B,Y.deserializeValue=u,y.zepto=Y,y}();window.Zepto=Zepto,"$"in window||(window.$=Zepto),function(a){function b(a){var b=this.os={},c=this.browser={},d=a.match(/WebKit\/([\d.]+)/),e=a.match(/(Android)\s+([\d.]+)/),f=a.match(/(iPad).*OS\s([\d_]+)/),g=!f&&a.match(/(iPhone\sOS)\s([\d_]+)/),h=a.match(/(webOS|hpwOS)[\s\/]([\d.]+)/),i=h&&a.match(/TouchPad/),j=a.match(/Kindle\/([\d.]+)/),k=a.match(/Silk\/([\d._]+)/),l=a.match(/(BlackBerry).*Version\/([\d.]+)/),m=a.match(/(BB10).*Version\/([\d.]+)/),n=a.match(/(RIM\sTablet\sOS)\s([\d.]+)/),o=a.match(/PlayBook/),p=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),q=a.match(/Firefox\/([\d.]+)/);(c.webkit=!!d)&&(c.version=d[1]),e&&(b.android=!0,b.version=e[2]),g&&(b.ios=b.iphone=!0,b.version=g[2].replace(/_/g,".")),f&&(b.ios=b.ipad=!0,b.version=f[2].replace(/_/g,".")),h&&(b.webos=!0,b.version=h[2]),i&&(b.touchpad=!0),l&&(b.blackberry=!0,b.version=l[2]),m&&(b.bb10=!0,b.version=m[2]),n&&(b.rimtabletos=!0,b.version=n[2]),o&&(c.playbook=!0),j&&(b.kindle=!0,b.version=j[1]),k&&(c.silk=!0,c.version=k[1]),!k&&b.android&&a.match(/Kindle Fire/)&&(c.silk=!0),p&&(c.chrome=!0,c.version=p[1]),q&&(c.firefox=!0,c.version=q[1]),b.tablet=!!(f||o||e&&!a.match(/Mobile/)||q&&a.match(/Tablet/)),b.phone=!b.tablet&&!!(e||g||h||l||m||p&&a.match(/Android/)||p&&a.match(/CriOS\/([\d.]+)/)||q&&a.match(/Mobile/))}b.call(a,navigator.userAgent),a.__detect=b}(Zepto),function(a){function b(a){return a._zid||(a._zid=n++)}function c(a,c,f,g){if(c=d(c),c.ns)var h=e(c.ns);return(m[b(a)]||[]).filter(function(a){return a&&(!c.e||a.e==c.e)&&(!c.ns||h.test(a.ns))&&(!f||b(a.fn)===b(f))&&(!g||a.sel==g)})}function d(a){var b=(""+a).split(".");return{e:b[0],ns:b.slice(1).sort().join(" ")}}function e(a){return new RegExp("(?:^| )"+a.replace(" "," .* ?")+"(?: |$)")}function f(b,c,d){"string"!=a.type(b)?a.each(b,d):b.split(/\s/).forEach(function(a){d(a,c)})}function g(a,b){return a.del&&("focus"==a.e||"blur"==a.e)||!!b}function h(a){return p[a]||a}function i(c,e,i,j,k,l){var n=b(c),o=m[n]||(m[n]=[]);f(e,i,function(b,e){var f=d(b);f.fn=e,f.sel=j,f.e in p&&(e=function(b){var c=b.relatedTarget;return!c||c!==this&&!a.contains(this,c)?f.fn.apply(this,arguments):void 0}),f.del=k&&k(e,b);var i=f.del||e;f.proxy=function(a){var b=i.apply(c,[a].concat(a.data));return b===!1&&(a.preventDefault(),a.stopPropagation()),b},f.i=o.length,o.push(f),c.addEventListener(h(f.e),f.proxy,g(f,l))})}function j(a,d,e,i,j){var k=b(a);f(d||"",e,function(b,d){c(a,b,d,i).forEach(function(b){delete m[k][b.i],a.removeEventListener(h(b.e),b.proxy,g(b,j))})})}function k(b){var c,d={originalEvent:b};for(c in b)!s.test(c)&&void 0!==b[c]&&(d[c]=b[c]);return a.each(t,function(a,c){d[a]=function(){return this[c]=q,b[a].apply(b,arguments)},d[c]=r}),d}function l(a){if(!("defaultPrevented"in a)){a.defaultPrevented=!1;var b=a.preventDefault;a.preventDefault=function(){this.defaultPrevented=!0,b.call(this)}}}var m=(a.zepto.qsa,{}),n=1,o={},p={mouseenter:"mouseover",mouseleave:"mouseout"};o.click=o.mousedown=o.mouseup=o.mousemove="MouseEvents",a.event={add:i,remove:j},a.proxy=function(c,d){if(a.isFunction(c)){var e=function(){return c.apply(d,arguments)};return e._zid=b(c),e}if("string"==typeof d)return a.proxy(c[d],c);throw new TypeError("expected function")},a.fn.bind=function(a,b){return this.each(function(){i(this,a,b)})},a.fn.unbind=function(a,b){return this.each(function(){j(this,a,b)})},a.fn.one=function(a,b){return this.each(function(c,d){i(this,a,b,null,function(a,b){return function(){var c=a.apply(d,arguments);return j(d,b,a),c}})})};var q=function(){return!0},r=function(){return!1},s=/^([A-Z]|layer[XY]$)/,t={preventDefault:"isDefaultPrevented",stopImmediatePropagation:"isImmediatePropagationStopped",stopPropagation:"isPropagationStopped"};a.fn.delegate=function(b,c,d){return this.each(function(e,f){i(f,c,d,b,function(c){return function(d){var e,g=a(d.target).closest(b,f).get(0);return g?(e=a.extend(k(d),{currentTarget:g,liveFired:f}),c.apply(g,[e].concat([].slice.call(arguments,1)))):void 0}})})},a.fn.undelegate=function(a,b,c){return this.each(function(){j(this,b,c,a)})},a.fn.live=function(b,c){return a(document.body).delegate(this.selector,b,c),this},a.fn.die=function(b,c){return a(document.body).undelegate(this.selector,b,c),this},a.fn.on=function(b,c,d){return!c||a.isFunction(c)?this.bind(b,c||d):this.delegate(c,b,d)},a.fn.off=function(b,c,d){return!c||a.isFunction(c)?this.unbind(b,c||d):this.undelegate(c,b,d)},a.fn.trigger=function(b,c){return("string"==typeof b||a.isPlainObject(b))&&(b=a.Event(b)),l(b),b.data=c,this.each(function(){"dispatchEvent"in this&&this.dispatchEvent(b)})},a.fn.triggerHandler=function(b,d){var e,f;return this.each(function(g,h){e=k("string"==typeof b?a.Event(b):b),e.data=d,e.target=h,a.each(c(h,b.type||b),function(a,b){return f=b.proxy(e),e.isImmediatePropagationStopped()?!1:void 0})}),f},"focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select keydown keypress keyup error".split(" ").forEach(function(b){a.fn[b]=function(a){return a?this.bind(b,a):this.trigger(b)}}),["focus","blur"].forEach(function(b){a.fn[b]=function(a){return a?this.bind(b,a):this.each(function(){try{this[b]()}catch(a){}}),this}}),a.Event=function(a,b){"string"!=typeof a&&(b=a,a=b.type);var c=document.createEvent(o[a]||"Events"),d=!0;if(b)for(var e in b)"bubbles"==e?d=!!b[e]:c[e]=b[e];return c.initEvent(a,d,!0,null,null,null,null,null,null,null,null,null,null,null,null),c.isDefaultPrevented=function(){return this.defaultPrevented},c}}(Zepto),function(a){function b(b,c,d){var e=a.Event(c);return a(b).trigger(e,d),!e.defaultPrevented}function c(a,c,d,e){return a.global?b(c||s,d,e):void 0}function d(b){b.global&&0===a.active++&&c(b,null,"ajaxStart")}function e(b){b.global&&!--a.active&&c(b,null,"ajaxStop")}function f(a,b){var d=b.context;return b.beforeSend.call(d,a,b)===!1||c(b,d,"ajaxBeforeSend",[a,b])===!1?!1:void c(b,d,"ajaxSend",[a,b])}function g(a,b,d){var e=d.context,f="success";d.success.call(e,a,f,b),c(d,e,"ajaxSuccess",[b,d,a]),i(f,b,d)}function h(a,b,d,e){var f=e.context;e.error.call(f,d,b,a),c(e,f,"ajaxError",[d,e,a]),i(b,d,e)}function i(a,b,d){var f=d.context;d.complete.call(f,b,a),c(d,f,"ajaxComplete",[b,d]),e(d)}function j(){}function k(a){return a&&(a=a.split(";",2)[0]),a&&(a==x?"html":a==w?"json":u.test(a)?"script":v.test(a)&&"xml")||"text"}function l(a,b){return(a+"&"+b).replace(/[&?]{1,2}/,"?")}function m(b){b.processData&&b.data&&"string"!=a.type(b.data)&&(b.data=a.param(b.data,b.traditional)),b.data&&(!b.type||"GET"==b.type.toUpperCase())&&(b.url=l(b.url,b.data))}function n(b,c,d,e){var f=!a.isFunction(c);return{url:b,data:f?c:void 0,success:f?a.isFunction(d)?d:void 0:c,dataType:f?e||d:d}}function o(b,c,d,e){var f,g=a.isArray(c);a.each(c,function(c,h){f=a.type(h),e&&(c=d?e:e+"["+(g?"":c)+"]"),!e&&g?b.add(h.name,h.value):"array"==f||!d&&"object"==f?o(b,h,d,c):b.add(c,h)})}var p,q,r=0,s=window.document,t=/)<[^<]*)*<\/script>/gi,u=/^(?:text|application)\/javascript/i,v=/^(?:text|application)\/xml/i,w="application/json",x="text/html",y=/^\s*$/;a.active=0,a.ajaxJSONP=function(b){if("type"in b){var c,d="jsonp"+ ++r,e=s.createElement("script"),i=function(){clearTimeout(c),a(e).remove(),delete window[d]},k=function(a){i(),a&&"timeout"!=a||(window[d]=j),h(null,a||"abort",l,b)},l={abort:k};return f(l,b)===!1?(k("abort"),!1):(window[d]=function(a){i(),g(a,l,b)},e.onerror=function(){k("error")},e.src=b.url.replace(/=\?/,"="+d),a("head").append(e),b.timeout>0&&(c=setTimeout(function(){k("timeout")},b.timeout)),l)}return a.ajax(b)},a.ajaxSettings={type:"GET",beforeSend:j,success:j,error:j,complete:j,context:null,global:!0,xhr:function(){return new window.XMLHttpRequest},accepts:{script:"text/javascript, application/javascript",json:w,xml:"application/xml, text/xml",html:x,text:"text/plain"},crossDomain:!1,timeout:0,processData:!0,cache:!0},a.ajax=function(b){var c=a.extend({},b||{});for(p in a.ajaxSettings)void 0===c[p]&&(c[p]=a.ajaxSettings[p]);d(c),c.crossDomain||(c.crossDomain=/^([\w-]+:)?\/\/([^\/]+)/.test(c.url)&&RegExp.$2!=window.location.host),c.url||(c.url=window.location.toString()),m(c),c.cache===!1&&(c.url=l(c.url,"_="+Date.now()));var e=c.dataType,i=/=\?/.test(c.url);if("jsonp"==e||i)return i||(c.url=l(c.url,"callback=?")),a.ajaxJSONP(c);var n,o=c.accepts[e],r={},s=/^([\w-]+:)\/\//.test(c.url)?RegExp.$1:window.location.protocol,t=c.xhr();c.crossDomain||(r["X-Requested-With"]="XMLHttpRequest"),o&&(r.Accept=o,o.indexOf(",")>-1&&(o=o.split(",",2)[0]),t.overrideMimeType&&t.overrideMimeType(o)),(c.contentType||c.contentType!==!1&&c.data&&"GET"!=c.type.toUpperCase())&&(r["Content-Type"]=c.contentType||"application/x-www-form-urlencoded"),c.headers=a.extend(r,c.headers||{}),t.onreadystatechange=function(){if(4==t.readyState){t.onreadystatechange=j,clearTimeout(n);var b,d=!1;if(t.status>=200&&t.status<300||304==t.status||0==t.status&&"file:"==s){e=e||k(t.getResponseHeader("content-type")),b=t.responseText;try{"script"==e?(1,eval)(b):"xml"==e?b=t.responseXML:"json"==e&&(b=y.test(b)?null:a.parseJSON(b))}catch(f){d=f}d?h(d,"parsererror",t,c):g(b,t,c)}else h(null,t.status?"error":"abort",t,c)}};var u="async"in c?c.async:!0;t.open(c.type,c.url,u);for(q in c.headers)t.setRequestHeader(q,c.headers[q]);return f(t,c)===!1?(t.abort(),!1):(c.timeout>0&&(n=setTimeout(function(){t.onreadystatechange=j,t.abort(),h(null,"timeout",t,c)},c.timeout)),t.send(c.data?c.data:null),t)},a.get=function(b,c,d,e){return a.ajax(n.apply(null,arguments))},a.post=function(b,c,d,e){var f=n.apply(null,arguments);return f.type="POST",a.ajax(f)},a.getJSON=function(b,c,d){var e=n.apply(null,arguments);return e.dataType="json",a.ajax(e)},a.fn.load=function(b,c,d){if(!this.length)return this;var e,f=this,g=b.split(/\s/),h=n(b,c,d),i=h.success;return g.length>1&&(h.url=g[0],e=g[1]),h.success=function(b){f.html(e?a("
").html(b.replace(t,"")).find(e):b),i&&i.apply(f,arguments)},a.ajax(h),this};var z=encodeURIComponent;a.param=function(a,b){var c=[];return c.add=function(a,b){this.push(z(a)+"="+z(b))},o(c,a,b),c.join("&").replace(/%20/g,"+")}}(Zepto),function(a){a.fn.serializeArray=function(){var b,c=[];return a(Array.prototype.slice.call(this.get(0).elements)).each(function(){b=a(this);var d=b.attr("type");"fieldset"!=this.nodeName.toLowerCase()&&!this.disabled&&"submit"!=d&&"reset"!=d&&"button"!=d&&("radio"!=d&&"checkbox"!=d||this.checked)&&c.push({name:b.attr("name"),value:b.val()})}),c},a.fn.serialize=function(){var a=[];return this.serializeArray().forEach(function(b){a.push(encodeURIComponent(b.name)+"="+encodeURIComponent(b.value))}),a.join("&")},a.fn.submit=function(b){if(b)this.bind("submit",b);else if(this.length){var c=a.Event("submit");this.eq(0).trigger(c),c.defaultPrevented||this.get(0).submit()}return this}}(Zepto),function(a,b){function c(a){return d(a.replace(/([a-z])([A-Z])/,"$1-$2"))}function d(a){return a.toLowerCase()}function e(a){return f?f+a:d(a)}var f,g,h,i,j,k,l,m,n="",o={Webkit:"webkit",Moz:"",O:"o",ms:"MS"},p=window.document,q=p.createElement("div"),r=/^((translate|rotate|scale)(X|Y|Z|3d)?|matrix(3d)?|perspective|skew(X|Y)?)$/i,s={};a.each(o,function(a,c){return q.style[a+"TransitionProperty"]!==b?(n="-"+d(a)+"-",f=c,!1):void 0}),g=n+"transform",s[h=n+"transition-property"]=s[i=n+"transition-duration"]=s[j=n+"transition-timing-function"]=s[k=n+"animation-name"]=s[l=n+"animation-duration"]=s[m=n+"animation-timing-function"]="",a.fx={off:f===b&&q.style.transitionProperty===b,speeds:{_default:400,fast:200,slow:600},cssPrefix:n,transitionEnd:e("TransitionEnd"),animationEnd:e("AnimationEnd")},a.fn.animate=function(b,c,d,e){return a.isPlainObject(c)&&(d=c.easing,e=c.complete,c=c.duration),c&&(c=("number"==typeof c?c:a.fx.speeds[c]||a.fx.speeds._default)/1e3),this.anim(b,c,d,e)},a.fn.anim=function(d,e,f,n){var o,p,q,t={},u="",v=this,w=a.fx.transitionEnd;if(e===b&&(e=.4),a.fx.off&&(e=0),"string"==typeof d)t[k]=d,t[l]=e+"s",t[m]=f||"linear",w=a.fx.animationEnd;else{p=[];for(o in d)r.test(o)?u+=o+"("+d[o]+") ":(t[o]=d[o],p.push(c(o)));u&&(t[g]=u,p.push(g)),e>0&&"object"==typeof d&&(t[h]=p.join(", "),t[i]=e+"s",t[j]=f||"linear")}return q=function(b){if("undefined"!=typeof b){if(b.target!==b.currentTarget)return;a(b.target).unbind(w,q)}a(this).css(s),n&&n.call(this)},e>0&&this.bind(w,q),this.size()&&this.get(0).clientLeft,this.css(t),0>=e&&setTimeout(function(){v.each(function(){q.call(this)})},0),this},q=null}(Zepto),function(a){function b(a){return"tagName"in a?a:a.parentNode}function c(a,b,c,d){var e=Math.abs(a-b),f=Math.abs(c-d);return e>=f?a-b>0?"Left":"Right":c-d>0?"Up":"Down"}function d(){j=null,k.last&&(k.el.trigger("longTap"),k={})}function e(){j&&clearTimeout(j),j=null}function f(){g&&clearTimeout(g),h&&clearTimeout(h),i&&clearTimeout(i),j&&clearTimeout(j),g=h=i=j=null,k={}}var g,h,i,j,k={},l=750;a(document).ready(function(){var m,n;a(document.body).bind("touchstart",function(c){m=Date.now(),n=m-(k.last||m),k.el=a(b(c.touches[0].target)),g&&clearTimeout(g),k.x1=c.touches[0].pageX,k.y1=c.touches[0].pageY,n>0&&250>=n&&(k.isDoubleTap=!0),k.last=m,j=setTimeout(d,l)}).bind("touchmove",function(a){e(),k.x2=a.touches[0].pageX,k.y2=a.touches[0].pageY,Math.abs(k.x1-k.x2)>10&&a.preventDefault()}).bind("touchend",function(b){e(),k.x2&&Math.abs(k.x1-k.x2)>30||k.y2&&Math.abs(k.y1-k.y2)>30?i=setTimeout(function(){k.el.trigger("swipe"),k.el.trigger("swipe"+c(k.x1,k.x2,k.y1,k.y2)),k={}},0):"last"in k&&(h=setTimeout(function(){var b=a.Event("tap");b.cancelTouch=f,k.el.trigger(b),k.isDoubleTap?(k.el.trigger("doubleTap"),k={}):g=setTimeout(function(){g=null,k.el.trigger("singleTap"),k={}},250)},0))}).bind("touchcancel",f),a(window).bind("scroll",f)}),["swipe","swipeLeft","swipeRight","swipeUp","swipeDown","doubleTap","tap","singleTap","longTap"].forEach(function(b){a.fn[b]=function(a){return this.bind(b,a)}})}(Zepto),FastClick.prototype.deviceIsAndroid=navigator.userAgent.indexOf("Android")>0,FastClick.prototype.deviceIsIOS=/iP(ad|hone|od)/.test(navigator.userAgent),FastClick.prototype.deviceIsIOS4=FastClick.prototype.deviceIsIOS&&/OS 4_\d(_\d)?/.test(navigator.userAgent),FastClick.prototype.deviceIsIOSWithBadTarget=FastClick.prototype.deviceIsIOS&&/OS ([6-9]|\d{2})_\d/.test(navigator.userAgent),FastClick.prototype.needsClick=function(a){"use strict";switch(a.nodeName.toLowerCase()){case"button":case"input":return this.deviceIsIOS&&"file"===a.type?!0:a.disabled;case"label":case"video":return!0;default:return/\bneedsclick\b/.test(a.className)}},FastClick.prototype.needsFocus=function(a){"use strict";switch(a.nodeName.toLowerCase()){case"textarea":case"select":return!0;case"input":switch(a.type){case"button":case"checkbox":case"file":case"image":case"radio":case"submit":return!1}return!a.disabled&&!a.readOnly;default:return/\bneedsfocus\b/.test(a.className)}},FastClick.prototype.sendClick=function(a,b){"use strict";var c,d;document.activeElement&&document.activeElement!==a&&document.activeElement.blur(),d=b.changedTouches[0],c=document.createEvent("MouseEvents"),c.initMouseEvent("click",!0,!0,window,1,d.screenX,d.screenY,d.clientX,d.clientY,!1,!1,!1,!1,0,null),c.forwardedTouchEvent=!0,a.dispatchEvent(c)},FastClick.prototype.focus=function(a){"use strict";var b;this.deviceIsIOS&&a.setSelectionRange?(b=a.value.length,a.setSelectionRange(b,b)):a.focus()},FastClick.prototype.updateScrollParent=function(a){"use strict";var b,c;if(b=a.fastClickScrollParent,!b||!b.contains(a)){c=a;do{if(c.scrollHeight>c.offsetHeight){b=c,a.fastClickScrollParent=c;break}c=c.parentElement}while(c)}b&&(b.fastClickLastScrollTop=b.scrollTop)},FastClick.prototype.getTargetElementFromEventTarget=function(a){"use strict";return a.nodeType===Node.TEXT_NODE?a.parentNode:a},FastClick.prototype.onTouchStart=function(a){"use strict";var b,c,d;if(b=this.getTargetElementFromEventTarget(a.target),c=a.targetTouches[0],this.deviceIsIOS){if(d=window.getSelection(),d.rangeCount&&!d.isCollapsed)return!0;if(!this.deviceIsIOS4){if(c.identifier===this.lastTouchIdentifier)return a.preventDefault(),!1;this.lastTouchIdentifier=c.identifier,this.updateScrollParent(b)}}return this.trackingClick=!0,this.trackingClickStart=a.timeStamp,this.targetElement=b,this.touchStartX=c.pageX,this.touchStartY=c.pageY,a.timeStamp-this.lastClickTime<200&&a.preventDefault(),!0},FastClick.prototype.touchHasMoved=function(a){"use strict";var b=a.changedTouches[0];return Math.abs(b.pageX-this.touchStartX)>10||Math.abs(b.pageY-this.touchStartY)>10?!0:!1},FastClick.prototype.findControl=function(a){"use strict";return void 0!==a.control?a.control:a.htmlFor?document.getElementById(a.htmlFor):a.querySelector("button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea")},FastClick.prototype.onTouchEnd=function(a){"use strict";var b,c,d,e,f,g=this.targetElement;if(this.touchHasMoved(a)&&(this.trackingClick=!1,this.targetElement=null),!this.trackingClick)return!0;if(a.timeStamp-this.lastClickTime<200)return this.cancelNextClick=!0,!0;if(this.lastClickTime=a.timeStamp,c=this.trackingClickStart,this.trackingClick=!1,this.trackingClickStart=0, -this.deviceIsIOSWithBadTarget&&(f=a.changedTouches[0],g=document.elementFromPoint(f.pageX-window.pageXOffset,f.pageY-window.pageYOffset)),d=g.tagName.toLowerCase(),"label"===d){if(b=this.findControl(g)){if(this.focus(g),this.deviceIsAndroid)return!1;g=b}}else if(this.needsFocus(g))return a.timeStamp-c>100||this.deviceIsIOS&&window.top!==window&&"input"===d?(this.targetElement=null,!1):(this.focus(g),this.deviceIsIOS4&&"select"===d||(this.targetElement=null,a.preventDefault()),!1);return this.deviceIsIOS&&!this.deviceIsIOS4&&(e=g.fastClickScrollParent,e&&e.fastClickLastScrollTop!==e.scrollTop)?!0:(this.needsClick(g)||(a.preventDefault(),this.sendClick(g,a)),!1)},FastClick.prototype.onTouchCancel=function(){"use strict";this.trackingClick=!1,this.targetElement=null},FastClick.prototype.onMouse=function(a){"use strict";return this.targetElement?a.forwardedTouchEvent?!0:a.cancelable&&(!this.needsClick(this.targetElement)||this.cancelNextClick)?(a.stopImmediatePropagation?a.stopImmediatePropagation():a.propagationStopped=!0,a.stopPropagation(),a.preventDefault(),!1):!0:!0},FastClick.prototype.onClick=function(a){"use strict";var b;return this.trackingClick?(this.targetElement=null,this.trackingClick=!1,!0):"submit"===a.target.type&&0===a.detail?!0:(b=this.onMouse(a),b||(this.targetElement=null),b)},FastClick.prototype.destroy=function(){"use strict";var a=this.layer;this.deviceIsAndroid&&(a.removeEventListener("mouseover",this.onMouse,!0),a.removeEventListener("mousedown",this.onMouse,!0),a.removeEventListener("mouseup",this.onMouse,!0)),a.removeEventListener("click",this.onClick,!0),a.removeEventListener("touchstart",this.onTouchStart,!1),a.removeEventListener("touchend",this.onTouchEnd,!1),a.removeEventListener("touchcancel",this.onTouchCancel,!1)},FastClick.attach=function(a){"use strict";return new FastClick(a)},"undefined"!=typeof define&&define.amd&&define(function(){"use strict";return FastClick}),"undefined"!=typeof module&&module.exports&&(module.exports=FastClick.attach,module.exports.FastClick=FastClick);var OSUR={util:{}};OSUR.util.hasTouchEvents=function(){var a;return("ontouchstart"in window||window.DocumentTouch&&document instanceof DocumentTouch)&&(a=!0),a},$(function(){$(".command-once").on("click",function(a){$.ajax({type:"POST",url:$(this).attr("href"),success:function(a){},error:function(a,b){}})}),$(".command-repeater").on("mousedown touchstart",function(a){$.ajax({type:"POST",url:$(this).attr("href")+"/send_start",success:function(a){},error:function(a,b){}}),$(this).attr("data-active",!0)}),$(".command-repeater").on("mouseup touchend touchleave touchcancel",function(a){$.ajax({type:"POST",url:$(this).attr("href")+"/send_stop",success:function(a){},error:function(a,b){}}),$(this).attr("data-active",!1)}),$(window).on("mouseup touchend touchleave touchcancel",function(a){$(".command-repeater[data-active=true]").trigger("mouseup")}),$(".macro-link").on("click",function(a){a.preventDefault(),$.ajax({type:"POST",url:$(this).attr("href"),success:function(a){},error:function(a,b){}})}),OSUR.util.hasTouchEvents()?($("body").addClass("has-touch"),$(".command-link, .remote-link").on("touchstart",function(a){$(this).addClass("active")}),$(".command-link, .remote-link").on("touchend touchleave touchcancel",function(a){$(this).removeClass("active")}),$("body").on("touchcancel",function(a){$(".command-link").removeClass("active")})):$("body").addClass("no-touch"),$(".back").on("click",function(a){$(".remote.active").removeClass("active"),$(".remotes-nav").removeClass("hidden"),$(".macros-nav").removeClass("hidden"),$(".back").addClass("hidden"),$("#title").html($("#title").attr("data-text")),$("#titlebar").removeClass("is-remote")}),$(".remotes-nav a").on("click",function(a){a.preventDefault();var b=$(this).attr("href");$(".remotes-nav").addClass("hidden"),$(".macros-nav").addClass("hidden"),$(b).addClass("active"),$(".back").removeClass("hidden"),$("#title").html($(this).html()),$("#titlebar").addClass("is-remote")}),OSUR.fastClick=new FastClick(document.body)}); \ No newline at end of file +this.deviceIsIOSWithBadTarget&&(f=a.changedTouches[0],g=document.elementFromPoint(f.pageX-window.pageXOffset,f.pageY-window.pageYOffset)),d=g.tagName.toLowerCase(),"label"===d){if(b=this.findControl(g)){if(this.focus(g),this.deviceIsAndroid)return!1;g=b}}else if(this.needsFocus(g))return a.timeStamp-c>100||this.deviceIsIOS&&window.top!==window&&"input"===d?(this.targetElement=null,!1):(this.focus(g),this.deviceIsIOS4&&"select"===d||(this.targetElement=null,a.preventDefault()),!1);return this.deviceIsIOS&&!this.deviceIsIOS4&&(e=g.fastClickScrollParent,e&&e.fastClickLastScrollTop!==e.scrollTop)?!0:(this.needsClick(g)||(a.preventDefault(),this.sendClick(g,a)),!1)},FastClick.prototype.onTouchCancel=function(){"use strict";this.trackingClick=!1,this.targetElement=null},FastClick.prototype.onMouse=function(a){"use strict";return this.targetElement?a.forwardedTouchEvent?!0:a.cancelable&&(!this.needsClick(this.targetElement)||this.cancelNextClick)?(a.stopImmediatePropagation?a.stopImmediatePropagation():a.propagationStopped=!0,a.stopPropagation(),a.preventDefault(),!1):!0:!0},FastClick.prototype.onClick=function(a){"use strict";var b;return this.trackingClick?(this.targetElement=null,this.trackingClick=!1,!0):"submit"===a.target.type&&0===a.detail?!0:(b=this.onMouse(a),b||(this.targetElement=null),b)},FastClick.prototype.destroy=function(){"use strict";var a=this.layer;this.deviceIsAndroid&&(a.removeEventListener("mouseover",this.onMouse,!0),a.removeEventListener("mousedown",this.onMouse,!0),a.removeEventListener("mouseup",this.onMouse,!0)),a.removeEventListener("click",this.onClick,!0),a.removeEventListener("touchstart",this.onTouchStart,!1),a.removeEventListener("touchend",this.onTouchEnd,!1),a.removeEventListener("touchcancel",this.onTouchCancel,!1)},FastClick.attach=function(a){"use strict";return new FastClick(a)},"undefined"!=typeof define&&define.amd&&define(function(){"use strict";return FastClick}),"undefined"!=typeof module&&module.exports&&(module.exports=FastClick.attach,module.exports.FastClick=FastClick);var OSUR={util:{}};OSUR.util.hasTouchEvents=function(){var a;return("ontouchstart"in window||window.DocumentTouch&&document instanceof DocumentTouch)&&(a=!0),a},$(function(){$(".command-once").on("click",function(a){$.ajax({type:"POST",url:$(this).attr("href"),success:function(a){},error:function(a,b){}})}),$(".command-repeater").on("mousedown touchstart",function(a){$.ajax({type:"POST",url:$(this).attr("href")+"/send_start",success:function(a){},error:function(a,b){}}),$(this).attr("data-active",!0)}),$(".command-repeater").on("mouseup touchend touchleave touchcancel",function(a){$.ajax({type:"POST",url:$(this).attr("href")+"/send_stop",success:function(a){},error:function(a,b){}}),$(this).attr("data-active",!1)}),$(window).on("mouseup touchend touchleave touchcancel",function(a){$(".command-repeater[data-active=true]").trigger("mouseup")}),$(".macro-link").on("click",function(a){a.preventDefault(),$.ajax({type:"POST",url:$(this).attr("href"),success:function(a){},error:function(a,b){}})}),OSUR.util.hasTouchEvents()?($("body").addClass("has-touch"),$(".command-link, .remote-link").on("touchstart",function(a){$(this).addClass("active")}),$(".command-link, .remote-link").on("touchend touchleave touchcancel",function(a){$(this).removeClass("active")}),$("body").on("touchcancel",function(a){$(".command-link").removeClass("active")})):$("body").addClass("no-touch"),$(".back").on("click",function(a){$(".remote.active").removeClass("active"),$(".macro.active").removeClass("active"),$(".remotes-nav").removeClass("hidden"),$(".macros-nav").removeClass("hidden"),$(".back").addClass("hidden"),$("#title").html($("#title").attr("data-text")),$("#titlebar").removeClass("is-remote"),$("#menubar").removeClass("is-remote")}),$(".remotes-navmenu a").on("click",function(a){a.preventDefault();var b=$(this).attr("href");$(".remotes-nav").addClass("hidden"),$(".macros-nav").addClass("hidden"),$(".remote.active").removeClass("active"),$(".macro.active").removeClass("active"),$(b).addClass("active"),$(".back").removeClass("hidden"),$("#title").html($(this).html()),$("#titlebar").addClass("is-remote"),$("#menubar").addClass("is-remote")}),$(".macros-navmenu a").on("click",function(a){a.preventDefault();var b=$(this).attr("href");$(".remotes-nav").addClass("hidden"),$(".macros-nav").addClass("hidden"),$(".remote.active").removeClass("active"),$(".macro.active").removeClass("active"),$(b).addClass("active"),$(".back").removeClass("hidden"),$("#title").html($(this).html()),$("#titlebar").addClass("is-remote"),$("#menubar").addClass("is-remote")}),OSUR.fastClick=new FastClick(document.body)}); \ No newline at end of file diff --git a/static/js/vendor/pushy.js b/static/js/vendor/pushy.js new file mode 100644 index 0000000..fb6a0f3 --- /dev/null +++ b/static/js/vendor/pushy.js @@ -0,0 +1,107 @@ +/*! Pushy - v0.9.2 - 2014-9-13 +* Pushy is a responsive off-canvas navigation menu using CSS transforms & transitions. +* https://github.com/christophery/pushy/ +* by Christopher Yee */ + +$(function() { + var pushy = $('.pushy'), //menu css class + body = $('body'), + container = $('#container'), //container css class + push = $('.push'), //css class to add pushy capability + siteOverlay = $('.site-overlay'), //site overlay + pushyClass = "pushy-left pushy-open", //menu position & menu open class + pushyActiveClass = "pushy-active", //css class to toggle site overlay + containerClass = "container-push", //container open class + pushClass = "push-push", //css class to add pushy capability + menuBtn = $('.menu-btn, .pushy a'), //css classes to toggle the menu + menuSpeed = 200, //jQuery fallback menu speed + menuWidth = pushy.width() + "px"; //jQuery fallback menu width + + function togglePushy(){ + body.toggleClass(pushyActiveClass); //toggle site overlay + pushy.toggleClass(pushyClass); + container.toggleClass(containerClass); + push.toggleClass(pushClass); //css class to add pushy capability + } + + function openPushyFallback(){ + body.addClass(pushyActiveClass); + pushy.animate({left: "0px"}, menuSpeed); + container.animate({left: menuWidth}, menuSpeed); + push.animate({left: menuWidth}, menuSpeed); //css class to add pushy capability + } + + function closePushyFallback(){ + body.removeClass(pushyActiveClass); + pushy.animate({left: "-" + menuWidth}, menuSpeed); + container.animate({left: "0px"}, menuSpeed); + push.animate({left: "0px"}, menuSpeed); //css class to add pushy capability + } + + //checks if 3d transforms are supported removing the modernizr dependency + cssTransforms3d = (function csstransforms3d(){ + var el = document.createElement('p'), + supported = false, + transforms = { + 'webkitTransform':'-webkit-transform', + 'OTransform':'-o-transform', + 'msTransform':'-ms-transform', + 'MozTransform':'-moz-transform', + 'transform':'transform' + }; + + // Add it to the body to get the computed style + document.body.insertBefore(el, null); + + for(var t in transforms){ + if( el.style[t] !== undefined ){ + el.style[t] = 'translate3d(1px,1px,1px)'; + supported = window.getComputedStyle(el).getPropertyValue(transforms[t]); + } + } + + document.body.removeChild(el); + + return (supported !== undefined && supported.length > 0 && supported !== "none"); + })(); + + if(cssTransforms3d){ + //toggle menu + menuBtn.click(function() { + togglePushy(); + }); + //close menu when clicking site overlay + siteOverlay.click(function(){ + togglePushy(); + }); + }else{ + //jQuery fallback + pushy.css({left: "-" + menuWidth}); //hide menu by default + container.css({"overflow-x": "hidden"}); //fixes IE scrollbar issue + + //keep track of menu state (open/close) + var state = true; + + //toggle menu + menuBtn.click(function() { + if (state) { + openPushyFallback(); + state = false; + } else { + closePushyFallback(); + state = true; + } + }); + + //close menu when clicking site overlay + siteOverlay.click(function(){ + if (state) { + openPushyFallback(); + state = false; + } else { + closePushyFallback(); + state = true; + } + }); + } +}); \ No newline at end of file diff --git a/static/js/vendor/pushy.min.js b/static/js/vendor/pushy.min.js new file mode 100644 index 0000000..16e4c18 --- /dev/null +++ b/static/js/vendor/pushy.min.js @@ -0,0 +1 @@ +$(function(){function a(){e.toggleClass(j),d.toggleClass(i),f.toggleClass(k),g.toggleClass(l)}function b(){e.addClass(j),d.animate({left:"0px"},n),f.animate({left:o},n),g.animate({left:o},n)}function c(){e.removeClass(j),d.animate({left:"-"+o},n),f.animate({left:"0px"},n),g.animate({left:"0px"},n)}var d=$(".pushy"),e=$("body"),f=$("#container"),g=$(".push"),h=$(".site-overlay"),i="pushy-left pushy-open",j="pushy-active",k="container-push",l="push-push",m=$(".menu-btn, .pushy a"),n=200,o=d.width()+"px";if(cssTransforms3d=function(){var a=document.createElement("p"),b=!1,c={webkitTransform:"-webkit-transform",OTransform:"-o-transform",msTransform:"-ms-transform",MozTransform:"-moz-transform",transform:"transform"};document.body.insertBefore(a,null);for(var d in c)void 0!==a.style[d]&&(a.style[d]="translate3d(1px,1px,1px)",b=window.getComputedStyle(a).getPropertyValue(c[d]));return document.body.removeChild(a),void 0!==b&&b.length>0&&"none"!==b}())m.click(function(){a()}),h.click(function(){a()});else{d.css({left:"-"+o}),f.css({"overflow-x":"hidden"});var p=!0;m.click(function(){p?(b(),p=!1):(c(),p=!0)}),h.click(function(){p?(b(),p=!1):(c(),p=!0)})}}); \ No newline at end of file diff --git a/templates/favicons.swig b/templates/favicons.swig new file mode 100644 index 0000000..53d819a --- /dev/null +++ b/templates/favicons.swig @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/index.swig b/templates/index.swig index 29be68f..0931806 100644 --- a/templates/index.swig +++ b/templates/index.swig @@ -12,11 +12,21 @@ +<<<<<<< HEAD + + + + + -
+ +
+ +
+ +
+
+ +

+ + Universal Remote +

+<<<<<<< HEAD +=======
    {% for macro in macros %} {% set macroName = loop.key %} -
  • +
  • {% endfor %}
+>>>>>>> refs/remotes/alexbain/master + {% for macro in macros %} + {% set area = loop.key %} +
    +

    {{area}}

    + {% for aMacroCommand in macro %} + {% set macroName = loop.key %} +
  • + {% endfor %} +
+ {% endfor %} + +
+<<<<<<< HEAD +
    + {% for macro in macros %} + {% set area = loop.key %} +
  • +
      + {% for command in macro %} + {% set macroName = loop.key %} +
    • + + {% endfor %} + +
    + {% endfor %} +
+ +
    + {% for remote in remotes %} + {% set remoteName = loop.key %} +
  • +
      + {% for command in remote %} +
    • + +
    • + {% endfor %} +
    +
  • +=======
      {% for remote in remotes %} {% set remoteName = loop.key %} @@ -45,13 +128,14 @@
        {% for command in remote %}
      • - +
      • {% endfor %}
      +>>>>>>> refs/remotes/alexbain/master {% endfor %} -
    +
@@ -59,8 +143,10 @@ + diff --git a/test/fixtures/config.json b/test/fixtures/config.json index 1a3e2b4..e67ead4 100644 --- a/test/fixtures/config.json +++ b/test/fixtures/config.json @@ -6,6 +6,34 @@ } }, "macros": { +<<<<<<< HEAD + "LivingRoom": { + "Play Xbox 360": [ + [ "SonyTV", "Power" ], + [ "SonyTV", "Xbox360" ], + [ "Yamaha", "Power" ], + [ "Yamaha", "Xbox360" ], + [ "Xbox360", "Power" ] + ], + "Listen to Music": [ + [ "Yamaha", "Power" ], + [ "Yamaha", "AirPlay" ] + ] + }, + "Downstairs": { + "Play Nintendo": [ + [ "SonyTV", "Power" ], + [ "SonyTV", "Xbox360" ], + [ "Yamaha", "Power" ], + [ "Yamaha", "Xbox360" ], + [ "Xbox360", "Power" ] + ], + "Listen to Radio": [ + [ "Yamaha", "Power" ], + [ "Yamaha", "AirPlay" ] + ] + } +======= "Play Xbox 360": [ [ "SonyTV", "Power" ], [ "SonyTV", "Xbox360" ], @@ -13,10 +41,15 @@ [ "Yamaha", "Xbox360" ], [ "Xbox360", "Power" ] ], - "Listen to Music": [ + "Listen to Music / Jams": [ [ "Yamaha", "Power" ], [ "Yamaha", "AirPlay" ] + ], + "Macro With Delay": [ + [ "delay", 500 ], + [ "Yamaha", "Power" ] ] +>>>>>>> refs/remotes/alexbain/master }, "commandLabels": { "LircNamespace": { @@ -28,6 +61,20 @@ } }, "remoteLabels": { +<<<<<<< HEAD + "LircNamespace": "LIRC namespace", + "Xbox360": "Xbox 360" +======= "LircNamespace": "LIRC namespace" + }, + "blacklists": { + "LightControl": [ + "S2", + "S4", + "S6", + "S7", + "S8" + ] +>>>>>>> refs/remotes/alexbain/master } } diff --git a/test/fixtures/remotes.json b/test/fixtures/remotes.json index 34fb34e..534d1ae 100644 --- a/test/fixtures/remotes.json +++ b/test/fixtures/remotes.json @@ -40,6 +40,16 @@ "A", "B" ], + "LightControl": [ + "S1", + "S2", + "S3", + "S4", + "S5", + "S6", + "S7", + "S8" + ], "LircNamespace": [ "KEY_POWER", "KEY_VOLUMEUP", diff --git a/test/lib/macros.js b/test/lib/macros.js new file mode 100644 index 0000000..9dd6784 --- /dev/null +++ b/test/lib/macros.js @@ -0,0 +1,62 @@ +var macros = require('../../lib/macros'); +var assert = require('assert'); +var sinon = require('sinon'); + +// config fixture +var config = require('../fixtures/config.json'); + +describe('macros', function () { + var lircNode; + var clock; + var stub; + + beforeEach(function (done) { + clock = sinon.useFakeTimers(); + + lircNode = {}; + lircNode.irsend = {}; + lircNode.irsend.send_once = function () {}; + + stub = sinon.stub(lircNode.irsend, 'send_once', function (remote, command, cb) { + cb(); + }); + + done(); + }); + + afterEach(function (done) { + clock.restore(); + + done(); + }); + + describe('exec', function () { + it('should call lircNode.irsend.send_once when executing a macro', function () { + macros.exec(config.macros['Play Xbox 360'], lircNode); + + assert.equal(stub.called, true); + }); + + it('should delay when encountering a delay', function () { + macros.exec(config.macros['Macro With Delay'], lircNode); + + assert.equal(stub.called, false); + + // not enough time has passed, stub should not have been called + clock.tick(250); + assert.equal(stub.called, false); + + // enough time has now passed, macro should have tried to execute next command + clock.tick(250); + assert.equal(stub.called, true); + }); + + it('should call send_once once per command', function () { + macros.exec(config.macros['Play Xbox 360'], lircNode); + // wait enough time + clock.tick(500); + + assert.equal(stub.callCount, 5); + }); + }); +}); diff --git a/test/lirc_web.js b/test/lirc_web.js index ec4360e..ea36a28 100644 --- a/test/lirc_web.js +++ b/test/lirc_web.js @@ -1,116 +1,164 @@ -var app = require('../app'), - assert = require('assert'), - request = require('supertest'), - sinon = require('sinon'); -jsdom = require("jsdom"); - - -describe('lirc_web', function() { - - describe('routes', function() { - - // Root route - it('should have an index route "/"', function(done) { - assert(request(app).get('/').expect(200, done)); - }); - - - // JSON API - it('should have GET route for JSON list of macros', function(done) { - assert(request(app).get('/macros.json').expect(200, done)); - }); +var app = require('../app'); +var assert = require('assert'); +var request = require('supertest'); +var jsdom = require('jsdom'); +var fs = require('fs'); +var jquery = fs.readFileSync('node_modules/jquery/dist/jquery.js', 'utf-8'); + +describe('lirc_web', function () { + describe('routes', function () { + // Root route + it('should have an index route "/"', function (done) { + assert(request(app).get('/').expect(200, done)); + }); - it('should have GET route for JSON list of remotes', function(done) { - assert(request(app).get('/remotes.json').expect(200, done)); - }); + // JSON API + it('should have GET route for JSON list of macros', function (done) { + assert(request(app).get('/macros.json').expect(200, done)); + }); - it('should have GET route for JSON list of commands for remote', function(done) { - assert(request(app).get('/remotes/Xbox360.json').expect(200, done)); - }); + it('should have GET route for JSON list of remotes', function (done) { + assert(request(app).get('/remotes.json').expect(200, done)); + }); +<<<<<<< HEAD it('should have GET route for JSON list of commands for macro', function(done) { - assert(request(app).get('/macros/Play%20Xbox%20360.json').expect(200, done)); + assert(request(app).get('/macros/Play Xbox 360.json').expect(200, done)); }); +======= + it('should have GET route for JSON list of commands for remote', function (done) { + assert(request(app).get('/remotes/Xbox360.json').expect(200, done)); + }); +>>>>>>> refs/remotes/alexbain/master - it('should return 404 for unknown remote', function(done) { - assert(request(app).get('/remotes/DOES_NOT_EXIST.json').expect(404, done)); - }); + it('should have GET route for JSON list of commands for macro', function (done) { + assert(request(app).get('/macros/Play%20Xbox%20360.json').expect(200, done)); + }); + it('should properly handle macros with / in them', function (done) { + assert(request(app).get('/macros/Listen%20to%20Music%20%2F%20Jams.json').expect(200, done)); + }); - // Sending commands - it('should have POST route for sending a command', function(done) { - assert(request(app).post('/remotes/tv/power').expect(200, done)); - }); + it('should return 404 for unknown remote', function (done) { + assert(request(app).get('/remotes/DOES_NOT_EXIST.json').expect(404, done)); + }); - it('should have POST route to start repeatedly sending a command', function(done) { - assert(request(app).post('/remotes/tv/volumeup/send_start').expect(200, done)); - }); + // Sending commands + it('should have POST route for sending a command', function (done) { + assert(request(app).post('/remotes/tv/power').expect(200, done)); + }); - it('should have POST route to stop repeatedly sending a command', function(done) { - assert(request(app).post('/remotes/tv/volumeup/send_stop').expect(200, done)); - }); + it('should have POST route to start repeatedly sending a command', function (done) { + assert(request(app).post('/remotes/tv/volumeup/send_start').expect(200, done)); + }); + it('should have POST route to stop repeatedly sending a command', function (done) { + assert(request(app).post('/remotes/tv/volumeup/send_stop').expect(200, done)); + }); +<<<<<<< HEAD // Sending macros it('should have POST route for sending a macro', function(done) { - assert(request(app).post('/macros/xbox_360').expect(200, done)); + assert(request(app).post('/macros/LivingRoom/Play Xbox 360').expect(200, done)); +======= + // Sending macros + it('should have POST route for sending a macro', function (done) { + assert(request(app).post('/macros/Play%20Xbox%20360').expect(200, done)); + }); + }); + + describe('index action', function () { + var error; + var response; + var $; + + before(function (done) { + request(app).get('/').end(function (err, res) { + error = err; + response = res; + jsdom.env({ + html: response.text, + src: [jquery], + done: function (errors, window) { + if (errors) { + console.log(errors); + } + $ = window.$; + done(); + }, +>>>>>>> refs/remotes/alexbain/master }); - + }); }); - describe('index action', function() { + it('should return an HTML document', function () { + assert(response.headers['content-type'].match(/html/)); + }); - it('should return an HTML document', function(done) { - assert(request(app).get('/').expect('Content-Type', /html/, done)); - }); + it('should return an HTML document in which all button elements of class command-link have an href of the form /remotes/:remote/:command', function () { + assert.equal(error, null); - it('should return an HTML document in which all button elements of class command-link have an href of the form /remotes/:remote/:command', function(done) { - request(app) - .get('/') - .end(function(err, res) { - assert.equal(err, null); - // TODO: Remove external dependency so offline development and testing is possible - jsdom.env(res.text, ["http://code.jquery.com/jquery.js"], function (errors, window) { - var $ = window.$; - $("button.command-link").each(function(idx, elem) { - var s = $(elem).attr('href').split("/"); - assert.equal(4, s.length); - assert.equal("", s[0]); - assert.equal("remotes", s[1]); - }); - done(); - }); - }); - }); + $('button.command-link').each(function (idx, elem) { + var s = $(elem).attr('href').split('/'); + assert.equal(4, s.length); + assert.equal('', s[0]); + assert.equal('remotes', s[1]); + }); }); - describe('json api', function() { - it('should return a list of all remotes (and commands) when /remotes.json is accessed', function(done) { - request(app) - .get('/remotes.json') - .end(function(err, res) { - assert.equal(res.status, 200); - done(); - }); - }); + it('should apply remotes configured labels', function () { + $('ul.remotes-nav').each(function (idx, elem) { + assert(elem.textContent.match(/LIRC namespace/) !== null); + assert(elem.textContent.match(/LircNamespace/) === null); + }); + }); + }); + + describe('json api', function () { + var XBOX_COMMANDS = ['OpenClose', 'FancyButton', 'OnOff', 'Stop', + 'Pause', 'Rewind', 'FastForward', 'Prev', 'Next', 'Play', + 'Display', 'Title', 'DVD_Menu', 'Back', 'Info', 'UpArrow', + 'LeftArrow', 'RightArrow', 'DownArrow', 'OK', 'Y', 'X', 'A', 'B']; + + var LIGHT_COMMANDS = ['S1', 'S3', 'S5']; + + var REFINED_REMOTES = { + Yamaha: ['Power', 'Xbox360', 'Wii', 'VolumeUp', 'VolumeDown', 'DTV/CBL'], + SonyTV: ['Power', 'VolumeUp', 'VolumeDown', 'ChannelUp', 'ChannelDown'], + Xbox360: XBOX_COMMANDS, + LightControl: LIGHT_COMMANDS, + LircNamespace: ['KEY_POWER', 'KEY_VOLUMEUP', 'KEY_VOLUMEDOWN', 'KEY_CHANNELUP', 'KEY_CHANNELDOWN'], + }; + + it('should return a list of all remotes (and commands) when /remotes.json is accessed', function (done) { + request(app) + .get('/remotes.json') + .set('Accept', 'application/json') + .expect(200, REFINED_REMOTES, done); + }); - it('should return a list of all commands for a remote when /remotes/:remote.json is accessed', function(done) { - request(app) - .get('/remotes/Xbox360.json') - .end(function(err, res) { - assert.equal(res.status, 200); - done(); - }); - }); + it('should return a list of all commands for a remote when /remotes/:remote.json is accessed', function (done) { + request(app) + .get('/remotes/Xbox360.json') + .set('Accept', 'application/json') + .expect(200, XBOX_COMMANDS, done); + }); - it('should return a 404 for an unknown remote', function(done) { - request(app) - .get('/remotes/DOES_NOT_EXIST.json') - .end(function(err, res) { - assert.equal(res.status, 404); - done(); - }); - }); + it('should return a filtered list of commands when a blacklist exists', function (done) { + request(app) + .get('/remotes/LightControl.json') + .set('Accept', 'application/json') + .expect(200, LIGHT_COMMANDS, done); }); + it('should return a 404 for an unknown remote', function (done) { + request(app) + .get('/remotes/DOES_NOT_EXIST.json') + .end(function (err, res) { + assert.equal(res.status, 404); + done(); + }); + }); + }); }); +// vim: tabstop=2:shiftwidth=2:sts=2:expandtab