├── .gitignore
├── .jshintrc
├── Gruntfile.js
├── README.md
├── lib
├── json-client.js
├── room.js
└── server.js
├── package.json
├── page
├── index.html
├── js
│ ├── eventListener.js
│ ├── main.js
│ └── recorder.js
├── jsx
│ └── app.jsx
├── static
│ ├── base.css
│ ├── gh-fork-ribbon.css
│ ├── logo.png
│ ├── recorderWorker.js
│ └── splash.jpg
└── vendor
│ ├── react.js
│ ├── recorder.js
│ ├── zepto.min.js
│ └── zepto.xcookie.min.js
└── tests
└── test_room.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .dev/
3 | dist/
4 | node_modules/
5 | npm-debug.log
6 | tmp/
7 | build/
8 | watchChanged.json
9 | config.json
10 |
--------------------------------------------------------------------------------
/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "curly": true,
3 | "eqeqeq": true,
4 | "immed": true,
5 | "latedef": true,
6 | "newcap": true,
7 | "noarg": true,
8 | "sub": true,
9 | "undef": true,
10 | "boss": true,
11 | "eqnull": true,
12 | "es5": true,
13 | "evil": true,
14 | "node": true,
15 | "browser": true,
16 | "strict": false
17 | }
18 |
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function(grunt) {
2 | var watchChanged = {}
3 | if (grunt.file.exists('watchChanged.json')) {
4 | watchChanged = grunt.file.readJSON('watchChanged.json')
5 | }
6 | grunt.initConfig({
7 | pkg: grunt.file.readJSON('package.json'),
8 | react: { // just for jsxhint, production transform is done
9 | // by browserify
10 | dynamic_mappings: {
11 | files: [
12 | {
13 | expand: true,
14 | cwd: 'page/jsx',
15 | src: ['*.jsx'],
16 | dest: 'tmp/jsx',
17 | ext: '.js'
18 | }
19 | ]
20 | }
21 | },
22 | jshint: {
23 | changed : [],
24 | js: ['Gruntfile.js', 'lib/*.js', 'page/js/*.js', 'tests/*.js'],
25 | jsx : ['tmp/jsx/*.js'],
26 | options: {
27 | "browser": true,
28 | "globals": {
29 | "React" : true,
30 | "CodeMirror" : true,
31 | "confirm" : true
32 | },
33 | "node" : true,
34 | "asi" : true,
35 | "globalstrict": false,
36 | "quotmark": false,
37 | "smarttabs": true,
38 | "trailing": false,
39 | "undef": true,
40 | "unused": false
41 | }
42 | },
43 | node_tap: {
44 | all: {
45 | options: {
46 | outputType: 'failures', // tap, failures, stats
47 | outputTo: 'console' // or file
48 | // outputFilePath: '/tmp/out.log' // path for output file,
49 | // only makes sense with outputTo 'file'
50 | },
51 | files: {
52 | 'tests': ['tests/*.js']
53 | }
54 | },
55 | changed: {
56 | options: {
57 | outputType: 'tap', // tap, failures, stats
58 | outputTo: 'console' // or file
59 | // outputFilePath: '/tmp/out.log' // path for output file,
60 | // only makes sense with outputTo 'file'
61 | },
62 | files: {
63 | 'tests': watchChanged.node_tap || []
64 | }
65 | }
66 | },
67 | copy: {
68 | assets: {
69 | files: [
70 | // includes files within path
71 | {expand: true, cwd: 'page/', src: ['*'], dest: 'build/', filter: 'isFile'},
72 |
73 | // includes files within path and its sub-directories
74 | {expand: true, cwd: 'page/static', src: ['*.js', '*.jpg', '*.css'], dest: 'build/'}
75 |
76 | // makes all src relative to cwd
77 | // {expand: true, cwd: 'path/', src: ['**'], dest: 'dest/'},
78 |
79 | // flattens results to a single level
80 | // {expand: true, flatten: true, src: ['path/**'], dest: 'dest/', filter: 'isFile'}
81 | ]
82 | }
83 | },
84 | browserify: {
85 | options: {
86 | debug : true,
87 | transform: [ require('grunt-react').browserify ]
88 | },
89 | app: {
90 | src: 'page/js/main.js',
91 | dest: 'build/bundle.js'
92 | }
93 | },
94 | uglify: {
95 | options: {
96 | mangle: false,
97 | compress : {
98 | unused : false
99 | },
100 | beautify : {
101 | ascii_only : true
102 | }
103 | },
104 | assets: {
105 | files: {
106 | // 'build/bundle.min.js': ['build/bundle.js'],
107 | 'build/vendor.min.js': ['page/vendor/*.js']
108 | }
109 | }
110 | },
111 | imageEmbed: {
112 | dist: {
113 | src: [ "page/static/base.css" ],
114 | dest: "build/base.css",
115 | options: {
116 | deleteAfterEncoding : false
117 | }
118 | }
119 | },
120 | // staticinline: {
121 | // main: {
122 | // files: {
123 | // 'build/index.html': 'build/index.html',
124 | // }
125 | // }
126 | // },
127 | express: {
128 | options: {
129 | // Override defaults here
130 | // delay : 100,
131 | // background: false,
132 | debug: true
133 | },
134 | dev: {
135 | options: {
136 | script: 'lib/server.js'
137 | }
138 | }
139 | },
140 | watch: {
141 | scripts: {
142 | files: ['Gruntfile.js', 'lib/**/*.js', 'page/js/*.js'],
143 | tasks: ['jshint:changed', 'default'],
144 | options: {
145 | spawn: false,
146 | },
147 | },
148 | jsx: {
149 | files: ['page/jsx/*.jsx'],
150 | tasks: ['jsxhint', 'default'],
151 | options: {
152 | spawn: false,
153 | },
154 | },
155 | other : {
156 | files: ['page/**/*'],
157 | tasks: ['default'],
158 | options: {
159 | spawn: false,
160 | },
161 | },
162 | tests : {
163 | files: ['tests/*.js'],
164 | tasks: ['jshint:js', 'node_tap:changed', 'default'],
165 | options: {
166 | interrupt: true,
167 | },
168 | },
169 | express: {
170 | files: [ 'lib/**/*.js' ],
171 | tasks: [ 'express:dev' ],
172 | options: {
173 | spawn: false
174 | }
175 | }
176 | },
177 | notify: {
178 | "watch": {
179 | options: {
180 | message: 'Assets compiled.', //required
181 | }
182 | }
183 | }
184 | })
185 | grunt.loadNpmTasks('grunt-newer');
186 | grunt.loadNpmTasks('grunt-browserify')
187 | grunt.loadNpmTasks('grunt-react');
188 | grunt.loadNpmTasks('grunt-contrib-jshint');
189 | grunt.loadNpmTasks('grunt-contrib-watch');
190 | grunt.loadNpmTasks('grunt-contrib-copy');
191 | grunt.loadNpmTasks('grunt-contrib-uglify');
192 | grunt.loadNpmTasks('grunt-node-tap');
193 | // grunt.loadNpmTasks('grunt-static-inline');
194 | grunt.loadNpmTasks("grunt-image-embed");
195 | grunt.loadNpmTasks('grunt-express-server');
196 | grunt.loadNpmTasks('grunt-notify');
197 |
198 | grunt.registerTask('jsxhint', ['newer:react', 'jshint:jsx']);
199 | grunt.registerTask('default', ['jshint:js', 'jsxhint', 'node_tap:all', 'build', 'notify']);
200 |
201 | grunt.registerTask("build", ['copy:assets', 'browserify', 'imageEmbed','uglify'])
202 |
203 | grunt.registerTask("dev", ["default", 'express:dev', 'watch'])
204 |
205 | grunt.event.on('watch', function(action, filepath) {
206 | // for (var key in require.cache) {delete require.cache[key];}
207 | grunt.config('jshint.changed', [filepath]);
208 | grunt.file.write("watchChanged.json", JSON.stringify({
209 | node_tap : [filepath]
210 | }))
211 | grunt.config('node_tap.changed.files.tests', [filepath]);
212 | });
213 | };
214 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CouchTalk
2 | ## Push To Talk example app
3 |
4 | CouchTalk is an example app showing off node.js with Couchbase Server. To run the app, do this:
5 |
6 | git clone
7 | cd couchtalk-nodejs
8 | npm install
9 | npm install -g grunt-cli
10 | grunt build
11 | npm start
12 |
13 | The last command will launch the server by running `node lib/server.js`
14 |
15 | ## Contributing
16 |
17 | If you edit the files under `lib/` or `page/` you need to run this to get the changes to show up.
18 |
19 | npm install -g grunt-cli
20 | grunt dev
21 |
22 | This will repackage the assets and launch the server in the background. It also watches the source files for changes and regenerates the assets and relaunches the server when you save files.
23 |
--------------------------------------------------------------------------------
/lib/json-client.js:
--------------------------------------------------------------------------------
1 | var requestLib = require("request"),
2 | request = requestLib.defaults({
3 | json:true
4 | }, function(uri, options, callback){
5 | var params = requestLib.initParams(uri, options, callback);
6 | // console.log("req", params.options)
7 | return requestLib(params.uri, params.options, function(err, res, body){
8 | // console.log("requestLib", err, res.statusCode, params.uri)
9 | // treat bad status codes as errors
10 | if (!err && res.statusCode >= 400) {
11 | params.callback.apply(this, [res.statusCode, res, body]);
12 | } else {
13 | params.callback.apply(this, arguments);
14 | }
15 | })
16 | });
17 |
18 | module.exports = request;
19 |
--------------------------------------------------------------------------------
/lib/room.js:
--------------------------------------------------------------------------------
1 | module.exports = function () {
2 | return {
3 | createMessage : function() {},
4 | recentMessages : function() {}
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/lib/server.js:
--------------------------------------------------------------------------------
1 | var express = require('express'),
2 | path = require("path"),
3 | fs = require("fs"),
4 | couchbase = require('couchbase'),
5 | build = path.join(__dirname, '..', 'build'),
6 | index = path.join(build, "index.html"),
7 | app = express(),
8 | server = require('http').createServer(app),
9 | io = require('socket.io').listen(server),
10 | EventEmitter = require("events").EventEmitter,
11 | portNum = 3000;
12 |
13 | var ee = new EventEmitter();
14 |
15 | try {
16 | var config = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'config.json'), "utf8"));
17 | } catch (e) {
18 | console.error(e)
19 | console.error("error loading config, using defaults")
20 | config = {bucket : "talk"}
21 | }
22 |
23 | console.log("connecting with config", config)
24 | var db = new couchbase.Connection(config, function(err) {
25 | if (err) throw err;
26 | console.log("connected")
27 |
28 | app.get("/loaderio-5298504f4a9c2e3dd950b17deb2f21bc/", function(req, res){
29 | res.send("loaderio-5298504f4a9c2e3dd950b17deb2f21bc")
30 | })
31 |
32 | app.get('/talk/:id', function(req, res){
33 | res.status(200).sendfile(index)
34 | });
35 |
36 | app.get("/snapshot/:id", function(req, res) {
37 | console.log("get snap", req.params.id)
38 | db.get(req.params.id, function(err, doc) {
39 | if (err) {
40 | res.status(404)
41 | res.json({error : "not_found"})
42 | } else {
43 | res.set('Content-Type', 'image/jpeg');
44 | res.set('Cache-Control', 'public, max-age=31536000');
45 | res.send(doc.value)
46 | }
47 | })
48 | })
49 |
50 | app.get("/audio/:id", function(req, res) {
51 | console.log("get audio", req.params.id)
52 | db.get(req.params.id, function(err, doc) {
53 | if (err) {
54 | res.status(404)
55 | res.json({error : "not_found"})
56 | } else {
57 | res.set('Content-Type', 'audio/wav');
58 | res.set('Cache-Control', 'public, max-age=31536000');
59 | res.send(doc.value)
60 | }
61 | })
62 | })
63 |
64 | app.post('/snapshot/:room_id/:snapshot_id/:keypress_id', function(req, res){
65 | console.log("post snap", req.params, req.query)
66 | var data = "";
67 |
68 | req.on('data', function(chunk) {
69 | data += chunk.toString();
70 | });
71 |
72 | req.on('end', function() {
73 | var ttl = req.query.selfDestruct,
74 | opts = {};
75 | if (ttl) {opts.expiry = parseInt(ttl, 10)}
76 | db.add(req.params.snapshot_id, new Buffer(data, "base64"), opts, function(err, result) {
77 | if (err) {
78 | res.status(500)
79 | console.log("no_update", err)
80 | res.json({error : "no_update"})
81 | } else {
82 | console.log("saved snap", req.params)
83 | ee.emit("room-"+req.params.room_id, {
84 | snap:req.params.snapshot_id,
85 | // keypressId:req.params.keypress_id,
86 | image : "true"
87 | })
88 | res.json({ok:true})
89 | }
90 | });
91 | })
92 | });
93 |
94 | app.post('/audio/:room_id/:snapshot_id/:keypressId', function(req, res){
95 | console.log("post audio", req.params, req.query)
96 | var data = "";
97 | req.on('data', function(chunk) {
98 | data += chunk.toString();
99 | });
100 |
101 | req.on('end', function() {
102 | var id = req.params.snapshot_id + "-audio",
103 | ttl = req.query.selfDestruct,
104 | opts = {};
105 | if (ttl) {opts.expiry = parseInt(ttl, 10)}
106 | console.log("audio id", id, opts)
107 | db.add(id, new Buffer(data, "base64"), opts, function(err, result) {
108 | if (err) {
109 | console.error(err)
110 | res.status(403)
111 | res.json({error : "no_update"})
112 | } else {
113 | ee.emit("room-"+req.params.room_id, {
114 | snap: req.params.snapshot_id,
115 | audio: id})
116 | res.json({ok:true, id : id})
117 | }
118 | });
119 | })
120 | });
121 |
122 | function getSnapshotId(room, cb) {
123 | db.incr("ct-"+room, {initial: 0}, function(err, result){
124 | cb(err, ["snap",room,result.value].join('-'))
125 | })
126 | }
127 |
128 | io.sockets.on('connection', function (socket) {
129 | socket.on('join', function (data) {
130 | console.log("join",data.room);
131 | ee.on("room-"+data.room, function(incoming) {
132 | console.log("ee emitted",incoming);
133 | socket.emit("message", incoming)
134 | })
135 | getSnapshotId(data.room, function(err, id){
136 | data.snap = id;
137 | console.log("snap id", data)
138 | // socket.emit("snap-id",data)
139 | ee.emit("room-"+data.room, data)
140 | })
141 | });
142 | // we need the session id to be part of the message
143 | socket.on('new-snap', function (data) {
144 | getSnapshotId(data.room, function(err, id){
145 | data.snap = id;
146 | socket.emit("message",data)
147 | })
148 | })
149 | });
150 |
151 | app.enable('trust proxy')
152 | app.use(express.static(build))
153 |
154 | console.log("listening on port", portNum)
155 | server.listen(portNum);
156 | });
157 |
158 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "CouchTalk",
3 | "scripts": {
4 | "start" : "node lib/server.js"
5 | },
6 | "dependencies": {
7 | "couchbase": "1.2.*",
8 | "express": "3.4.*",
9 | "async" : "0.2.10",
10 | "request" : "~2.34.0",
11 | "getusermedia" : "0.2.1",
12 | "socket.io" : "0.9.*"
13 | },
14 | "devDependencies": {
15 | "grunt": "~0.4.2",
16 | "grunt-contrib-jshint": "~0.6.3",
17 | "grunt-contrib-uglify": "~0.2.7",
18 | "grunt-react": "~0.6.0",
19 | "browserify": "~3.19.1",
20 | "grunt-browserify": "~1.3.0",
21 | "grunt-newer": "~0.6.0",
22 | "grunt-contrib-watch": "~0.5.3",
23 | "grunt-contrib-copy": "~0.5.0",
24 | "grunt-node-tap": "~0.1.52",
25 | "tap": "~0.4.8",
26 | "tape": "~2.3.2",
27 | "grunt-image-embed": "~0.3.1",
28 | "grunt-express-server": "^0.4.13",
29 | "grunt-notify": "^0.2.17"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/page/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CouchTalk with Couchbase
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Welcome to CouchTalk
13 |

14 |
15 |
16 |
21 |
22 |
23 |
24 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/page/js/eventListener.js:
--------------------------------------------------------------------------------
1 | // Apache 2.0 License http://www.apache.org/licenses/LICENSE-2.0.html
2 | // Copywrite 2014 Couchbase, Inc.
3 |
4 | module.exports = {
5 | listen : function(emitter, event, handler) {
6 | // console.log("listen", event)
7 | var mixinStateKeyForEvent = "_EventListenerMixinState:"+event;
8 | var sub = this.state[mixinStateKeyForEvent] || {};
9 | if (sub.event && sub.emitter) {
10 | if (sub.event == event && sub.emitter === emitter) {
11 | // we are already listening, noop
12 | // console.log("EventListenerMixin alreadyListening", sub.event, this)
13 | return;
14 | } else {
15 | // unsubscribe from the existing one
16 | // console.log("EventListenerMixin removeListener", sub.event, this)
17 | sub.emitter.removeListener(sub.event, sub.handler)
18 | }
19 | }
20 | var mixinState = {
21 | emitter : emitter,
22 | event : event,
23 | handler : handler
24 | }
25 | // console.log("EventListenerMixin addListener", event, this, mixinState)
26 | var stateToMerge = {};
27 | stateToMerge[mixinStateKeyForEvent] = mixinState;
28 | this.setState(stateToMerge);
29 | emitter.on(event, handler)
30 | },
31 | componentWillUnmount : function() {
32 | // console.log("componentWillUnmount", JSON.stringify(this.state))
33 | for (var eventKey in this.state) {
34 | var ekps = eventKey.split(":")
35 | if (ekps[0] == "_EventListenerMixinState") {
36 | var sub = this.state[eventKey]
37 | var emitter = sub.emitter
38 | // console.log("EventListenerMixin Unmount removeListener", eventKey, sub, this)
39 | emitter.removeListener(sub.event, sub.handler)
40 | }
41 | }
42 | },
43 | }
44 |
--------------------------------------------------------------------------------
/page/js/main.js:
--------------------------------------------------------------------------------
1 | /* global $ */
2 | var CouchTalk = require("../jsx/app.jsx");
3 |
4 | $(function () {
5 | var match = /\/talk\/(.*)/.exec(location.pathname);
6 | if (match) {
7 | React.renderComponent(
8 | CouchTalk.App({id : match[1]}),
9 | document.getElementById('container')
10 | );
11 | } else {
12 | React.renderComponent(CouchTalk.Index({}),
13 | document.getElementById("container"))
14 | }
15 | })
16 |
--------------------------------------------------------------------------------
/page/js/recorder.js:
--------------------------------------------------------------------------------
1 | /* global Recorder */
2 | /* global AudioContext */
3 |
4 | module.exports = {
5 | connectAudio : connectAudio
6 | }
7 |
8 | function connectAudio(cb) {
9 | window.AudioContext = window.AudioContext || window.webkitAudioContext;
10 | navigator.getUserMedia = navigator.getUserMedia ||
11 | navigator.webkitGetUserMedia ||
12 | navigator.mozGetUserMedia ||
13 | navigator.msGetUserMedia;
14 | window.URL = window.URL || window.webkitURL;
15 |
16 | if (!navigator.getUserMedia) {
17 | cb(new Error("navigator.getUserMedia missing"))
18 | } else {
19 | try {
20 | var audio_context = new AudioContext();
21 | } catch (e) {
22 | cb(new Error("AudioContext missing"))
23 | }
24 | navigator.getUserMedia({audio: true, video: true}, function(stream){
25 | var input = audio_context.createMediaStreamSource(stream),
26 | recorder = new Recorder(input, {workerPath: "/recorderWorker.js"});
27 | recorder.stream = stream;
28 | cb(false, recorder)
29 | }, function(e) {
30 | cb(new Error('No live audio input: ' + e));
31 | });
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/page/jsx/app.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * @jsx React.DOM
3 | */
4 | /* global $ */
5 | /* global io */
6 |
7 |
8 | var
9 | connectAudio = require("../js/recorder").connectAudio,
10 | getUserMedia = require("getusermedia");
11 |
12 | module.exports.App = React.createClass({
13 | propTypes : {
14 | id : React.PropTypes.string.isRequired,
15 | },
16 | getInitialState : function(){
17 | // console.log($.fn.cookie("autoplay"), $.fn.cookie("selfDestruct"), $.fn.cookie("selfDestructTTL"))
18 | var start = getQueryVariable("start");
19 | var end = getQueryVariable("end");
20 | // console.log(start, end)
21 | return {
22 | recording: false,
23 | messages : [],
24 | session : "s:"+Math.random().toString(20),
25 | autoplay : $.fn.cookie('autoplay') !== "false",
26 | selfDestructTTL : parseInt($.fn.cookie('selfDestructTTL'), 10) || 300,
27 | selfDestruct : $.fn.cookie('selfDestruct') === "true",
28 | nowPlaying : false,
29 | start : parseInt(start, 10),
30 | end : parseInt(end, 10)
31 | }
32 | },
33 | componentWillMount: function() {
34 | var socket = io.connect(location.origin)
35 | socket.on("message", this.gotMessage)
36 | this.setState({socket : socket})
37 | },
38 | gotMessage : function(message){
39 | var messages = this.state.messages;
40 | // console.log("message", message, messages.length)
41 |
42 | if (message.snap) {
43 | for (var i = messages.length - 1; i >= 0; i--) {
44 | if (messages[i] && messages[i].snap && messages[i].snap.split(":")[0] === message.snap.split(":")[0]) {
45 | break;
46 | }
47 | }
48 | if (messages[i]) {
49 | // exists in some form
50 | $.extend(messages[i], message)
51 | } else {
52 | // check to see if there's a message with the keypress
53 | if (message.keypressId) {
54 | var findIndex = {};
55 | this.messageForKeypress(message.keypressId, findIndex)
56 | if (findIndex.i) {
57 | $.extend(messages[findIndex.i], message)
58 | i = findIndex.i
59 | } else {
60 | // first time, add it
61 | messages.push(message)
62 | i = messages.length-1;
63 | }
64 | } else {
65 | // first time, add it
66 | messages.push(message)
67 | i = messages.length-1;
68 | }
69 |
70 | }
71 | this.setState({messages : messages})
72 | this.maybePlay(messages[i], i)
73 | } else {
74 | // console.log("no snap only keypressId", message)
75 | }
76 | },
77 | maybePlay : function(message, i) {
78 | // console.log(this.state, message)
79 | if (this.state.autoplay && this.state.nowPlaying === false) {
80 | if (this.state.session !== message.session) {
81 | this.playMessage(i)
82 | }
83 | }
84 | },
85 | listenForSpaceBar : function(){
86 | // record while spacebar is down
87 | window.onkeydown = function (e) {
88 | var code = e.keyCode ? e.keyCode : e.which;
89 | if (code === 32) { //spacebar
90 | e.preventDefault()
91 | this.startRecord("kp:"+Math.random().toString(20))
92 | }
93 | }.bind(this);
94 | window.onkeyup = function (e) {
95 | var code = e.keyCode ? e.keyCode : e.which;
96 | if (code === 32) { // spacebar
97 | this.stopRecord()
98 | }
99 | }.bind(this);
100 | },
101 | startRecord : function(keypressId) {
102 | if (this.state.recording) return;
103 | // console.log("startRecord",keypressId)
104 | this.state.recorder.record()
105 | var counter = 0;
106 | this.takeSnapshot(keypressId, counter)
107 | var interval = setInterval(function(){
108 | this.takeSnapshot(keypressId, counter++)
109 | }.bind(this), 250)
110 | var video = $("video")
111 | video.addClass("recording")
112 | video.data("keypressId", keypressId)
113 | this.setState({recording : true, pictureInterval : interval});
114 | this.state.socket.emit("new-snap", {
115 | keypressId : keypressId,
116 | session : this.state.session,
117 | room : this.props.id
118 | })
119 | },
120 | stopRecord : function() {
121 | if (this.state.pictureInterval) clearInterval(this.state.pictureInterval)
122 | if (!this.state.recording) {
123 | return console.error("I thought I was recording!")
124 | }
125 | var video = $("video"),
126 | keypressId = video.data("keypressId"),
127 | recorder = this.state.recorder;
128 | recorder.stop()
129 | video.removeClass("recording");
130 | var message = this.messageForKeypress(keypressId);
131 | if (message) {
132 | delete message.snapdata;
133 | }
134 |
135 | recorder.exportMonoWAV(this.saveAudio.bind(this, keypressId))
136 | recorder.clear()
137 | this.setState({recording : false})
138 | // console.log("stopped recording", keypressId)
139 | },
140 | saveAudio : function(keypressId, wav){
141 | this.messageWithIdForKeypress(keypressId,
142 | function(message){
143 | var reader = new FileReader(),
144 | postURL = "/audio/" + this.props.id + "/" + message.snap.split(":")[0] + "/" + keypressId;
145 | if (this.state.selfDestruct) {
146 | postURL+= "?selfDestruct="+this.state.selfDestructTTL;
147 | }
148 | reader.addEventListener("loadend", function() {
149 | var parts = reader.result.split(/[,;:]/)
150 | $.ajax({
151 | type : "POST",
152 | url : postURL,
153 | contentType : parts[1],
154 | data : parts[3],
155 | success : function() {
156 | // console.log("saved audio", message)
157 | }
158 | })
159 | }.bind(this));
160 | reader.readAsDataURL(wav);
161 | }.bind(this))
162 | },
163 | takeSnapshot : function(keypressId, counter){
164 | var rootNode = $(this.getDOMNode());
165 | var canvas = rootNode.find("canvas")[0];
166 | var video = rootNode.find("video")[0];
167 | var ctx = canvas.getContext('2d');
168 | ctx.drawImage(video, 0, 0, video.width*2, video.height*2);
169 | this.saveSnapshot(canvas.toDataURL("image/jpeg"), keypressId, counter);
170 | },
171 | messageForKeypress : function(keypressId, index) {
172 | var messages = this.state.messages;
173 | // console.log('messageForKeypress', keypressId, messages)
174 | for (var i = messages.length - 1; i >= 0; i--) {
175 | var m = messages[i];
176 | if (m.keypressId == keypressId) {
177 | if (index) {index.i = i;}
178 | return m;
179 | }
180 | }
181 | },
182 | messageWithIdForKeypress : function(keypressId, cb, retries){
183 | var message = this.messageForKeypress(keypressId);
184 | retries = retries || 0;
185 | if (!(message && message.snap)) { // we haven't got the id via socket.io
186 | if (retries < 100) {
187 | // console.log("wait for snap id for", keypressId, message, retries)
188 | setTimeout(this.messageWithIdForKeypress.bind(this,
189 | keypressId, cb, retries+1), 100*(retries+1))
190 | } else {
191 | console.error("too many retries", keypressId, message)
192 | }
193 | } else { // we are good
194 | cb(message)
195 | }
196 | },
197 | saveLocalSnapshot : function(png, keypressId){
198 | var messages = this.state.messages;
199 | var findIndex = {}
200 | var existingMessage = this.messageForKeypress(keypressId, findIndex);
201 | if (existingMessage) {
202 | // console.log("update local snapshot", existingMessage)
203 | messages[findIndex.i].snapdata = png;
204 | this.setState({messages : messages});
205 | } else {
206 | var newMessage = {keypressId : keypressId, snapdata : png}
207 | messages.push(newMessage)
208 | // console.log("save new local snapshot", newMessage)
209 | this.setState({messages : messages});
210 | }
211 | },
212 | saveSnapshot : function(png, keypressId, counter){
213 | // make locally available before saved on server
214 | this.saveLocalSnapshot(png, keypressId)
215 |
216 | // save on server as soon as we have an id
217 | this.messageWithIdForKeypress(keypressId,
218 | function(message){
219 | // console.log("save pic", message)
220 | var parts = png.split(/[,;:]/),
221 | picId = message.snap.split(":")[0];
222 | if (counter) {
223 | picId += ":" + counter
224 | }
225 | var postURL = "/snapshot/" + this.props.id + "/" + picId + "/" + keypressId;
226 | if (this.state.selfDestruct) {
227 | postURL+= "?selfDestruct="+this.state.selfDestructTTL;
228 | }
229 | $.ajax({
230 | type : "POST",
231 | url : postURL,
232 | contentType : parts[1],
233 | data : parts[3],
234 | success : function(data) {
235 | // console.log("saved snap", message)
236 | }
237 | })
238 | }.bind(this))
239 | },
240 | pleasePlayMessage : function(i){
241 | // console.log("pleasePlayMessage", i)
242 | if (this.state.nowPlaying !== false) {
243 | var rootNode = $(this.getDOMNode());
244 | // play the audio from the beginning
245 | var audio = rootNode.find("audio")[this.state.nowPlaying]
246 | // console.log("should stop", audio)
247 | audio.load() // fires ended event?
248 | }
249 | this.setState({nowPlaying : false})
250 | this.playMessage(i)
251 | },
252 | playMessage : function(i){
253 | // todo move to message, remove `i`
254 | var message = this.state.messages[i];
255 | if (!this.state.recording && message) {
256 | if (message.audio) {
257 | var rootNode = $(this.getDOMNode());
258 | // play the audio from the beginning
259 |
260 | var audio = rootNode.find("audio")[i]
261 | audio.load()
262 | audio.play()
263 | message.played = true
264 | // console.log(audio, audio.ended, audio.networkState)
265 | setTimeout(function() {
266 | // console.log(audio, audio.ended, audio.networkState)
267 | if (audio.networkState != 1) {
268 | this.playFinished(message)
269 | }
270 | }.bind(this),1000)
271 |
272 | this.setState({
273 | nowPlaying : i,
274 | messages : this.state.messages})
275 | } else {
276 | this.playMessage(i+1)
277 | }
278 | } else {
279 | this.setState({nowPlaying : false})
280 | }
281 | },
282 | playFinished : function(message){
283 | if (this.state.autoplay) {
284 | // find the index of the current message in the messages array
285 | // by snap. then play the next one
286 | var messages = this.state.messages;
287 | for (var i = messages.length - 1; i >= 0; i--) {
288 | if (messages[i].snap == message.snap) {
289 | break;
290 | }
291 | }
292 | this.playMessage(i + 1)
293 | } else {
294 | this.setState({nowPlaying : false})
295 | }
296 | },
297 | setupAudioVideo : function(rootNode, recorder){
298 | var video = $(rootNode).find("video")[0]
299 | video.muted = true;
300 | video.src = window.URL.createObjectURL(recorder.stream)
301 | },
302 | autoPlayChanged : function(e){
303 | $.fn.cookie('autoplay', e.target.checked.toString(), {path : "/"});
304 | this.setState({autoplay: e.target.checked})
305 | },
306 | loadConversation : function(start, end){
307 | console.log("conversation view", start, end)
308 | var room = this.props.id,
309 | oldMessages = [], min = Math.min(start, end);
310 | for (var i = end; i >= start; i--) {
311 | oldMessages.unshift({
312 | snap : ["snap",room,i].join("-"),
313 | audio : ["snap",room,i,"audio"].join("-"),
314 | image : true
315 | })
316 | }
317 | this.setState({messages : oldMessages.concat(this.state.messages)})
318 | },
319 | loadEarlierMessages : function(start, end){
320 | var room = this.props.id, oldest = this.state.messages[0],
321 | before;
322 | if (oldest && oldest.snap) {
323 | before = parseInt(oldest.snap.split('-')[2], 10)
324 | }
325 | var oldMessages = [], min = Math.max(before - 10, 0)
326 | for (var i = before - 1; i >= min; i--) {
327 | oldMessages.unshift({
328 | snap : ["snap",room,i].join("-"),
329 | audio : ["snap",room,i,"audio"].join("-"),
330 | image : true
331 | })
332 | }
333 | this.setState({messages : oldMessages.concat(this.state.messages)})
334 | },
335 | componentDidMount : function(rootNode){
336 | if (this.state.start && this.state.end) {
337 | this.loadConversation(this.state.start, this.state.end)
338 | }
339 | connectAudio(function(error, recorder) {
340 | if (error) {return reloadError(error)}
341 | this.setupAudioVideo(rootNode, recorder)
342 | this.listenForSpaceBar()
343 | this.state.socket.emit("join", {
344 | keypressId : this.state.session,
345 | session : this.state.session,
346 | room : this.props.id,
347 | join : true
348 | })
349 | if (this.state.start && this.state.end && this.state.autoplay) {
350 | this.playMessage(0)
351 | }
352 | setTimeout(function(){
353 | this.takeSnapshot(this.state.session)
354 | }.bind(this), 1000)
355 | this.setState({recorder: recorder})
356 | }.bind(this))
357 | },
358 | componentDidUpdate : function(oldProps, oldState){
359 | var el, els = $(".room img.playing")
360 | if (els[0]) {
361 | el = els[0]
362 | } else {
363 | if (oldState.messages[oldState.messages.length-1] && (oldState.messages[oldState.messages.length-1].snap !==
364 | this.state.messages[this.state.messages.length-1].snap)) {
365 | els = $(".room img")
366 | el = els[els.length-1]
367 | }
368 | } // todo check for did the user scroll recently
369 | if (el) {el.scrollIntoView(true)}
370 | },
371 | selfDestructTTLChanged : function(e) {
372 | $.fn.cookie('selfDestructTTL', e.target.value, {path : "/"});
373 | this.setState({selfDestructTTL:e.target.value})
374 | },
375 | selfDestructChanged : function(e) {
376 | $.fn.cookie('selfDestruct', e.target.checked.toString(), {path : "/"});
377 | this.setState({selfDestruct:e.target.checked})
378 | },
379 | render : function() {
380 | var url = location.origin + "/talk/" + this.props.id;
381 | var recording = this.state.recording ?
382 | Recording. :
383 | ;
384 | var oldestKnownMessage = this.state.messages[0];
385 | document.title = this.props.id + " - CouchTalk"
386 | var beg = this.state.recorder ? "" : Smile! ⇑
;
387 | return (
388 |
389 |
413 |
414 | {this.state.messages.map(function(m, i) {
415 | return
422 | }, this)}
423 |
424 |
425 | );
426 | }
427 | })
428 |
429 |
430 | var RecentRooms = React.createClass({
431 | getInitialState : function(){
432 | return {
433 | sortedRooms : this.sortedRooms()
434 | }
435 | },
436 | parseRooms : function(){
437 | var rooms = $.fn.cookie("rooms");
438 | if (rooms) {
439 | return JSON.parse(rooms)
440 | } else {
441 | return {}
442 | }
443 | },
444 | sortedRooms : function() {
445 | var rooms = this.parseRooms()
446 | var sortedRooms = [];
447 | for (var room in rooms) {
448 | if (room !== this.props.id)
449 | sortedRooms.push([room, new Date(rooms[room])])
450 | }
451 | if (sortedRooms.length > 0) {
452 | sortedRooms.sort(function(a, b) {return b[1] - a[1]})
453 | // console.log("sortedRooms", sortedRooms)
454 | return sortedRooms;
455 | }
456 | },
457 | clearHistory : function(){
458 | $.fn.cookie("rooms", '{}', {path : "/"})
459 | this.setState({sortedRooms : this.sortedRooms()})
460 | },
461 | componentDidMount : function(){
462 | if (this.props.id) {
463 | var rooms = this.parseRooms()
464 | // console.log("parseRooms", rooms)
465 | rooms[this.props.id] = new Date();
466 | $.fn.cookie("rooms", JSON.stringify(rooms), {path : "/"})
467 | }
468 | },
469 | render : function(){
470 | if (this.state.sortedRooms) {
471 | return
480 | } else {
481 | return
482 | }
483 | },
484 | })
485 |
486 | exports.Index = React.createClass({
487 | getInitialState : function(){
488 | return {goRoom : Math.random().toString(26).substr(2)}
489 | },
490 | onSubmit : function(e){
491 | e.preventDefault();
492 | document.location = "/talk/" + this.state.goRoom;
493 | },
494 | handleChange : function(e){
495 | this.setState({goRoom: e.target.value});
496 | },
497 | render : function(){
498 | return (
499 |
Welcome to CouchTalk
500 |
Enter the name of a room to join:
501 |
507 |

508 |
509 |
)
510 | }
511 | })
512 |
513 |
514 | var Message = React.createClass({
515 | getInitialState : function(){
516 | return {showing : 0, previous : 0}
517 | },
518 | componentDidMount : function(){
519 | var audio = $(this.getDOMNode()).find("audio")[0];
520 | audio.addEventListener('ended', function(){
521 | this.stopAnimation()
522 | this.props.playFinished(this.props.message)
523 | }.bind(this))
524 | audio.addEventListener('playing', function(){
525 | this.animateImages()
526 | }.bind(this))
527 | var deck = $(this.getDOMNode()).find("img.ondeck")[0];
528 | deck.addEventListener("load", function(e) {
529 | // console.log("deck load", this, e)
530 | $(this).parent().find("img.messImg")[0].src = this.src;
531 | })
532 | },
533 | // shouldComponentUpdate : function(nextProps) {
534 | // // console.log(nextProps.message)
535 | // warning
536 | // return ["snap","audio","played","playing","image"].filter(function(k){
537 | // console.log(k, nextProps.message[k], this.props.message[k])
538 | // return nextProps.message[k] !== this.props.message[k]
539 | // }.bind(this)).length !== 0
540 | // // return true;
541 | // },
542 | getMax : function(){
543 | var split = this.props.message.snap.split(":");
544 | if (split[1]) {
545 | return parseInt(split[1], 10) || 0;
546 | } else { return 0}
547 | },
548 | getSnapURLs : function(){
549 | var url = "/snapshot/" + this.props.message.snap.split(":")[0];
550 | var number = this.props.message.audio ? this.state.showing : this.getMax();
551 | var oldURL = url, oldURLNum;
552 | if (number) {
553 | oldURLNum = number - 1;
554 | url += ":" + number
555 | if (oldURLNum) {
556 | oldURL += ":" + oldURLNum
557 | }
558 | }
559 | return {
560 | now : url,
561 | prev : oldURL
562 | };
563 | },
564 | animateImages : function() {
565 | var animateHandle = setInterval(function(){
566 | this.setState({showing : this.state.showing+1})
567 | }.bind(this), 250)
568 | this.setState({animateHandle : animateHandle})
569 | },
570 | stopAnimation: function(){
571 | clearInterval(this.state.animateHandle)
572 | this.setState({showing : 0})
573 | },
574 | render : function() {
575 | // console.log("Render", this.props.message)
576 | var snapURL, backupURL, snapURLs;
577 | if (this.props.message.image) {
578 | snapURLs = this.getSnapURLs();
579 | backupURL = snapURLs.prev;
580 | snapURL = snapURLs.now;
581 | }
582 | if (this.props.message.snapdata) {
583 | snapURL = this.props.message.snapdata;
584 | }
585 | var audioURL = "/audio/" + this.props.message.audio;
586 | var className = "messImg";
587 |
588 | if (!this.props.message.audio) {
589 | if (!this.props.message.join) {
590 | className += " noAudio"
591 | }
592 | } else {
593 | if (this.props.playing) {
594 | className += " playing"
595 | } else {
596 | if (this.props.message.played) {
597 | className += " played"
598 | } else {
599 | className += " unplayed"
600 | }
601 | }
602 | }
603 | return (
604 |
605 |
606 | )
608 | }
609 | })
610 |
611 | function reloadError(error) {
612 | if (navigator.getUserMedia) {
613 | console.error("reload",error);
614 | setTimeout(function(){
615 | document.location = location
616 | },200)
617 | } else {
618 | $("h2").html('CouchTalk requires Firefox or Chrome!')
619 | }
620 | }
621 |
622 | function getQueryVariable(variable) {
623 | var query = window.location.search.substring(1);
624 | var vars = query.split("&");
625 | for (var i=0; i < vars.length; i++) {
626 | var pair = vars[i].split("=");
627 | if (pair[0] == variable) {
628 | return pair[1];
629 | }
630 | }
631 | }
632 |
--------------------------------------------------------------------------------
/page/static/base.css:
--------------------------------------------------------------------------------
1 | /* global tags */
2 | * {
3 | box-sizing: border-box;
4 | }
5 |
6 | body {
7 | background: #fff;
8 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;;
9 | /*font-size: 15px;*/
10 | margin: 0;
11 | padding: 0;
12 | }
13 |
14 | code {
15 | background-color: #f8f8f8;
16 | border: 1px solid #ddd;
17 | border-radius: 3px;
18 | font-family: "Bitstream Vera Sans Mono", Consolas, Courier, monospace;
19 | font-size: 12px;
20 | margin: 0 2px;
21 | padding: 0px 5px;
22 | }
23 |
24 | h1, h2, h3, h4 {
25 | font-weight: bold;
26 | margin: 0 0 15px;
27 | padding: 0;
28 | }
29 |
30 | h1 {
31 | border-bottom: 1px solid #ddd;
32 | font-size: 2.5em;
33 | font-weight: bold;
34 | margin: 0 0 15px;
35 | padding: 0;
36 | }
37 |
38 | h2 {
39 | border-bottom: 1px solid #eee;
40 | font-size: 2em;
41 | }
42 |
43 | h3 {
44 | font-size: 1.5em;
45 | }
46 |
47 | h4 {
48 | font-size: 1.2em;
49 | }
50 |
51 | p, ul {
52 | margin: 0;
53 | }
54 |
55 | ul {
56 | /*list-style-type: none;*/
57 | }
58 |
59 | a {
60 | color: #4183c4;
61 | text-decoration: none;
62 | }
63 |
64 | a.active {
65 | font-weight: bold;
66 | }
67 |
68 | a:hover {
69 | /*color: #94651a;*/
70 | text-decoration: underline;
71 | }
72 |
73 | .clear {
74 | clear:both;
75 | }
76 |
77 | /* layout */
78 |
79 | #container {
80 | margin:0;
81 | padding:4px;
82 | }
83 |
84 | /* CouchTalk */
85 |
86 | #splash {
87 | text-align: center;
88 | }
89 |
90 | #splash input {
91 | font-size: 200%
92 | }
93 |
94 | #splash form button {
95 | font-size: 200%
96 | }
97 |
98 | .recording {
99 | color:#aa0003;
100 | }
101 |
102 | header {
103 | position: fixed;
104 | top: 0;
105 | right: 0;
106 | width: 400px;
107 | padding:6px;
108 | border-left: 4px dotted #f4f2f7;
109 | background: #fff;
110 | }
111 |
112 | header h4 {
113 | clear:left;
114 | }
115 |
116 | header h2 {
117 | text-align: right;
118 | color: #ffac35;
119 | margin-right: 1em;
120 | }
121 |
122 | aside{
123 | margin-top: 2em;
124 | font-size: 86%;
125 | color:#888;
126 | }
127 |
128 | .shareLink {
129 | width: 100%;
130 | }
131 |
132 | video {
133 | border: 4px solid #000;
134 | box-sizing: content-box;
135 | float: left;
136 | margin-right: 12px;
137 | margin-bottom: 12px;
138 | }
139 |
140 | video.recording {
141 | border: 4px solid #f00;
142 | }
143 |
144 | ul.messages {
145 | margin-right: 400px;
146 | list-style-type: none;
147 | padding-left: 0;
148 | }
149 |
150 |
151 | ul.messages li {
152 | display: inline-block;
153 | margin: 0; padding: 4px;
154 | }
155 |
156 | ul.messages li img {
157 | border: 4px solid #fff;
158 | box-sizing: content-box;
159 | width:160px;
160 | height:120px;
161 | }
162 |
163 | img.ondeck {
164 | display: none;
165 | }
166 |
167 | ul.messages li img.playing {
168 | border: 4px solid #0f0;
169 | }
170 |
171 | ul.messages li img.played {
172 | border: 4px solid #aca;
173 | }
174 |
175 | ul.messages li img.unplayed {
176 | border: 4px solid #0a0;
177 | }
178 |
179 | ul.messages li img.noAudio {
180 | border: 4px solid #800;
181 | background: #000;
182 | }
183 |
184 | .autoplay {
185 | color:#0b0;
186 | }
187 |
--------------------------------------------------------------------------------
/page/static/gh-fork-ribbon.css:
--------------------------------------------------------------------------------
1 | /* Left will inherit from right (so we don't need to duplicate code) */
2 | .github-fork-ribbon {
3 | /* The right and left classes determine the side we attach our banner to */
4 | position: absolute;
5 |
6 | /* Add a bit of padding to give some substance outside the "stitching" */
7 | padding: 2px 0;
8 |
9 | /* Set the base colour */
10 | background-color: #090;
11 |
12 | /* Set a gradient: transparent black at the top to almost-transparent black at the bottom */
13 | background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0)), to(rgba(0, 0, 0, 0.15)));
14 | background-image: -webkit-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15));
15 | background-image: -moz-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15));
16 | background-image: -ms-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15));
17 | background-image: -o-linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15));
18 | background-image: linear-gradient(top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.15));
19 |
20 | /* Add a drop shadow */
21 | -webkit-box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.5);
22 | -moz-box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.5);
23 | box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.5);
24 |
25 | z-index: 9999;
26 | pointer-events: auto;
27 | }
28 |
29 | .github-fork-ribbon a,
30 | .github-fork-ribbon a:hover {
31 | /* Set the font */
32 | font: 700 13px "Helvetica Neue", Helvetica, Arial, sans-serif;
33 | color: #fff;
34 |
35 | /* Set the text properties */
36 | text-decoration: none;
37 | text-shadow: 0 -1px rgba(0, 0, 0, 0.5);
38 | text-align: center;
39 |
40 | /* Set the geometry. If you fiddle with these you'll also need
41 | to tweak the top and right values in .github-fork-ribbon. */
42 | width: 200px;
43 | line-height: 20px;
44 |
45 | /* Set the layout properties */
46 | display: inline-block;
47 | padding: 2px 0;
48 |
49 | /* Add "stitching" effect */
50 | border-width: 1px 0;
51 | border-style: dotted;
52 | border-color: #fff;
53 | border-color: rgba(255, 255, 255, 0.7);
54 | }
55 |
56 | .github-fork-ribbon-wrapper {
57 | width: 150px;
58 | height: 150px;
59 | position: absolute;
60 | overflow: hidden;
61 | top: 0;
62 | z-index: 9999;
63 | pointer-events: none;
64 | }
65 |
66 | .github-fork-ribbon-wrapper.fixed {
67 | position: fixed;
68 | }
69 |
70 | .github-fork-ribbon-wrapper.right {
71 | right: 0;
72 | }
73 |
74 | .github-fork-ribbon-wrapper.right-bottom {
75 | position: fixed;
76 | top: inherit;
77 | bottom: 0;
78 | right: 0;
79 | }
80 |
81 | .github-fork-ribbon-wrapper.right .github-fork-ribbon {
82 | top: 42px;
83 | right: -43px;
84 |
85 | -webkit-transform: rotate(45deg);
86 | -moz-transform: rotate(45deg);
87 | -ms-transform: rotate(45deg);
88 | -o-transform: rotate(45deg);
89 | transform: rotate(45deg);
90 | }
91 |
92 | .github-fork-ribbon-wrapper.right-bottom .github-fork-ribbon {
93 | top: 80px;
94 | right: -43px;
95 |
96 | -webkit-transform: rotate(-45deg);
97 | -moz-transform: rotate(-45deg);
98 | -ms-transform: rotate(-45deg);
99 | -o-transform: rotate(-45deg);
100 | transform: rotate(-45deg);
101 | }
102 |
--------------------------------------------------------------------------------
/page/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/couchtalk-node/54f9bb0605fe6e7a2f052d6d10c1f2ebc0b8065f/page/static/logo.png
--------------------------------------------------------------------------------
/page/static/recorderWorker.js:
--------------------------------------------------------------------------------
1 | var recLength = 0,
2 | recBuffersL = [],
3 | recBuffersR = [],
4 | sampleRate;
5 |
6 | this.onmessage = function(e){
7 | switch(e.data.command){
8 | case 'init':
9 | init(e.data.config);
10 | break;
11 | case 'record':
12 | record(e.data.buffer);
13 | break;
14 | case 'exportWAV':
15 | exportWAV(e.data.type);
16 | break;
17 | case 'exportMonoWAV':
18 | exportMonoWAV(e.data.type);
19 | break;
20 | case 'getBuffer':
21 | getBuffer();
22 | break;
23 | case 'clear':
24 | clear();
25 | break;
26 | }
27 | };
28 |
29 | function init(config){
30 | sampleRate = config.sampleRate;
31 | }
32 |
33 | function record(inputBuffer){
34 | recBuffersL.push(inputBuffer[0]);
35 | recBuffersR.push(inputBuffer[1]);
36 | recLength += inputBuffer[0].length;
37 | }
38 |
39 | function exportWAV(type){
40 | var bufferL = mergeBuffers(recBuffersL, recLength);
41 | var bufferR = mergeBuffers(recBuffersR, recLength);
42 | var interleaved = interleave(bufferL, bufferR);
43 | var dataview = encodeWAV(interleaved);
44 | var audioBlob = new Blob([dataview], { type: type });
45 |
46 | this.postMessage(audioBlob);
47 | }
48 |
49 | function exportMonoWAV(type){
50 | var bufferL = mergeBuffers(recBuffersL, recLength);
51 | var dataview = encodeMonoWAV(bufferL);
52 | var audioBlob = new Blob([dataview], { type: type });
53 |
54 | this.postMessage(audioBlob);
55 | }
56 |
57 | function getBuffer() {
58 | var buffers = [];
59 | buffers.push( mergeBuffers(recBuffersL, recLength) );
60 | buffers.push( mergeBuffers(recBuffersR, recLength) );
61 | this.postMessage(buffers);
62 | }
63 |
64 | function clear(){
65 | recLength = 0;
66 | recBuffersL = [];
67 | recBuffersR = [];
68 | }
69 |
70 | function mergeBuffers(recBuffers, recLength){
71 | var result = new Float32Array(recLength);
72 | var offset = 0;
73 | for (var i = 0; i < recBuffers.length; i++){
74 | result.set(recBuffers[i], offset);
75 | offset += recBuffers[i].length;
76 | }
77 | return result;
78 | }
79 |
80 | function interleave(inputL, inputR){
81 | var length = inputL.length + inputR.length;
82 | var result = new Float32Array(length);
83 |
84 | var index = 0,
85 | inputIndex = 0;
86 |
87 | while (index < length){
88 | result[index++] = inputL[inputIndex];
89 | result[index++] = inputR[inputIndex];
90 | inputIndex++;
91 | }
92 | return result;
93 | }
94 |
95 | function floatTo16BitPCM(output, offset, input){
96 | for (var i = 0; i < input.length; i++, offset+=2){
97 | var s = Math.max(-1, Math.min(1, input[i]));
98 | output.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
99 | }
100 | }
101 |
102 | function floatTo8BitPCM(output, offset, input){
103 | for (var i = 0; i < input.length; i++, offset++){
104 | var s = Math.max(-1, Math.min(1, input[i]));
105 | output.setInt8(offset, (s * 128) + 128, true);
106 | }
107 | }
108 |
109 |
110 | function writeString(view, offset, string){
111 | for (var i = 0; i < string.length; i++){
112 | view.setUint8(offset + i, string.charCodeAt(i));
113 | }
114 | }
115 |
116 | function encodeWAV(samples){
117 | var buffer = new ArrayBuffer(44 + samples.length * 2);
118 | var view = new DataView(buffer);
119 |
120 | /* RIFF identifier */
121 | writeString(view, 0, 'RIFF');
122 | /* file length */
123 | view.setUint32(4, 32 + samples.length * 2, true);
124 | /* RIFF type */
125 | writeString(view, 8, 'WAVE');
126 | /* format chunk identifier */
127 | writeString(view, 12, 'fmt ');
128 | /* format chunk length */
129 | view.setUint32(16, 16, true);
130 | /* sample format (raw) */
131 | view.setUint16(20, 1, true);
132 | /* channel count */
133 | view.setUint16(22, 2, true);
134 | /* sample rate */
135 | view.setUint32(24, sampleRate, true);
136 | /* byte rate (sample rate * block align) */
137 | view.setUint32(28, sampleRate * 4, true);
138 | /* block align (channel count * bytes per sample) */
139 | view.setUint16(32, 4, true);
140 | /* bits per sample */
141 | view.setUint16(34, 16, true);
142 | /* data chunk identifier */
143 | writeString(view, 36, 'data');
144 | /* data chunk length */
145 | view.setUint32(40, samples.length * 2, true);
146 |
147 | floatTo16BitPCM(view, 44, samples);
148 |
149 | return view;
150 | }
151 |
152 | function encodeMonoWAV(samples){
153 | var buffer = new ArrayBuffer(44 + samples.length * 2);
154 | var view = new DataView(buffer);
155 |
156 | /* RIFF identifier */
157 | writeString(view, 0, 'RIFF');
158 | /* file length */
159 | view.setUint32(4, 32 + samples.length, true);
160 | /* RIFF type */
161 | writeString(view, 8, 'WAVE');
162 | /* format chunk identifier */
163 | writeString(view, 12, 'fmt ');
164 | /* format chunk length */
165 | view.setUint32(16, 16, true);
166 | /* sample format (raw) */
167 | view.setUint16(20, 1, true);
168 | /* channel count */
169 | view.setUint16(22, 1, true);
170 | /* sample rate */
171 | view.setUint32(24, sampleRate, true);
172 | /* byte rate (sample rate * block align) */
173 | view.setUint32(28, sampleRate * 2, true);
174 | /* block align (channel count * bytes per sample) */
175 | view.setUint16(32, 2, true);
176 | /* bits per sample */
177 | view.setUint16(34, 16, true);
178 | /* data chunk identifier */
179 | writeString(view, 36, 'data');
180 | /* data chunk length */
181 | view.setUint32(40, samples.length * 2, true);
182 |
183 | floatTo16BitPCM(view, 44, samples);
184 |
185 | return view;
186 | }
187 |
--------------------------------------------------------------------------------
/page/static/splash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/couchbaselabs/couchtalk-node/54f9bb0605fe6e7a2f052d6d10c1f2ebc0b8065f/page/static/splash.jpg
--------------------------------------------------------------------------------
/page/vendor/recorder.js:
--------------------------------------------------------------------------------
1 | (function(window){
2 |
3 | var WORKER_PATH = 'recorderWorker.js';
4 |
5 | var Recorder = function(source, cfg){
6 | var config = cfg || {};
7 | var bufferLen = config.bufferLen || 4096;
8 | this.context = source.context;
9 | this.node = (this.context.createScriptProcessor ||
10 | this.context.createJavaScriptNode).call(this.context,
11 | bufferLen, 2, 2);
12 | var worker = new Worker(config.workerPath || WORKER_PATH);
13 | worker.postMessage({
14 | command: 'init',
15 | config: {
16 | sampleRate: this.context.sampleRate
17 | }
18 | });
19 | var recording = false,
20 | currCallback;
21 |
22 | this.node.onaudioprocess = function(e){
23 | if (!recording) return;
24 | worker.postMessage({
25 | command: 'record',
26 | buffer: [
27 | e.inputBuffer.getChannelData(0),
28 | e.inputBuffer.getChannelData(1)
29 | ]
30 | });
31 | }
32 |
33 | this.configure = function(cfg){
34 | for (var prop in cfg){
35 | if (cfg.hasOwnProperty(prop)){
36 | config[prop] = cfg[prop];
37 | }
38 | }
39 | }
40 |
41 | this.record = function(){
42 | recording = true;
43 | }
44 |
45 | this.stop = function(){
46 | recording = false;
47 | }
48 |
49 | this.clear = function(){
50 | worker.postMessage({ command: 'clear' });
51 | }
52 |
53 | this.getBuffer = function(cb) {
54 | currCallback = cb || config.callback;
55 | worker.postMessage({ command: 'getBuffer' })
56 | }
57 |
58 | this.exportWAV = function(cb, type){
59 | currCallback = cb || config.callback;
60 | type = type || config.type || 'audio/wav';
61 | if (!currCallback) throw new Error('Callback not set');
62 | worker.postMessage({
63 | command: 'exportWAV',
64 | type: type
65 | });
66 | }
67 |
68 | this.exportMonoWAV = function(cb, type){
69 | currCallback = cb || config.callback;
70 | type = type || config.type || 'audio/wav';
71 | if (!currCallback) throw new Error('Callback not set');
72 | worker.postMessage({
73 | command: 'exportMonoWAV',
74 | type: type
75 | });
76 | }
77 |
78 | worker.onmessage = function(e){
79 | var blob = e.data;
80 | currCallback(blob);
81 | }
82 |
83 | source.connect(this.node);
84 | this.node.connect(this.context.destination); //this should not be necessary
85 | };
86 |
87 | Recorder.forceDownload = function(blob, filename){
88 | var url = (window.URL || window.webkitURL).createObjectURL(blob);
89 | var link = window.document.createElement('a');
90 | link.href = url;
91 | link.download = filename || 'output.wav';
92 | var click = document.createEvent("Event");
93 | click.initEvent("click", true, true);
94 | link.dispatchEvent(click);
95 | }
96 |
97 | window.Recorder = Recorder;
98 |
99 | })(window);
100 |
--------------------------------------------------------------------------------
/page/vendor/zepto.min.js:
--------------------------------------------------------------------------------
1 | /* Zepto v1.0-1-ga3cab6c - polyfill zepto detect event ajax form fx - zeptojs.com/license */
2 | (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(this===void 0||this===null)throw new TypeError;var c=Object(this),d=c.length>>>0,e=0,f;if(typeof b!="function")throw new TypeError;if(d==0&&arguments.length==1)throw new TypeError;if(arguments.length>=2)f=arguments[1];else do{if(e in c){f=c[e++];break}if(++e>=d)throw new TypeError}while(!0);while(e0?c.fn.concat.apply([],a):a}function O(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 P(a){return a in j?j[a]:j[a]=new RegExp("(^|\\s)"+a+"(\\s|$)")}function Q(a,b){return typeof b=="number"&&!l[O(a)]?b+"px":b}function R(a){var b,c;return i[a]||(b=h.createElement(a),h.body.appendChild(b),c=k(b,"").getPropertyValue("display"),b.parentNode.removeChild(b),c=="none"&&(c="block"),i[a]=c),i[a]}function S(a){return"children"in a?f.call(a.children):c.map(a.childNodes,function(a){if(a.nodeType==1)return a})}function T(c,d,e){for(b in d)e&&(J(d[b])||K(d[b]))?(J(d[b])&&!J(c[b])&&(c[b]={}),K(d[b])&&!K(c[b])&&(c[b]=[]),T(c[b],d[b],e)):d[b]!==a&&(c[b]=d[b])}function U(b,d){return d===a?c(b):c(b).filter(d)}function V(a,b,c,d){return F(b)?b.call(a,c,d):b}function W(a,b,c){c==null?a.removeAttribute(b):a.setAttribute(b,c)}function X(b,c){var d=b.className,e=d&&d.baseVal!==a;if(c===a)return e?d.baseVal:d;e?d.baseVal=c:b.className=c}function Y(a){var b;try{return a?a=="true"||(a=="false"?!1:a=="null"?null:isNaN(b=Number(a))?/^[\[\{]/.test(a)?c.parseJSON(a):a:b):a}catch(d){return a}}function Z(a,b){b(a);for(var c in a.childNodes)Z(a.childNodes[c],b)}var a,b,c,d,e=[],f=e.slice,g=e.filter,h=window.document,i={},j={},k=h.defaultView.getComputedStyle,l={"column-count":1,columns:1,"font-weight":1,"line-height":1,opacity:1,"z-index":1,zoom:1},m=/^\s*<(\w+|!)[^>]*>/,n=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,o=/^(?:body|html)$/i,p=["val","css","html","text","data","width","height","offset"],q=["after","prepend","before","append"],r=h.createElement("table"),s=h.createElement("tr"),t={tr:h.createElement("tbody"),tbody:r,thead:r,tfoot:r,td:s,th:s,"*":h.createElement("div")},u=/complete|loaded|interactive/,v=/^\.([\w-]+)$/,w=/^#([\w-]*)$/,x=/^[\w-]+$/,y={},z=y.toString,A={},B,C,D=h.createElement("div");return A.matches=function(a,b){if(!a||a.nodeType!==1)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=D).appendChild(a),d=~A.qsa(e,b).indexOf(a),f&&D.removeChild(a),d},B=function(a){return a.replace(/-+(.)?/g,function(a,b){return b?b.toUpperCase():""})},C=function(a){return g.call(a,function(b,c){return a.indexOf(b)==c})},A.fragment=function(b,d,e){b.replace&&(b=b.replace(n,"<$1>$2>")),d===a&&(d=m.test(b)&&RegExp.$1),d in t||(d="*");var g,h,i=t[d];return i.innerHTML=""+b,h=c.each(f.call(i.childNodes),function(){i.removeChild(this)}),J(e)&&(g=c(h),c.each(e,function(a,b){p.indexOf(a)>-1?g[a](b):g.attr(a,b)})),h},A.Z=function(a,b){return a=a||[],a.__proto__=c.fn,a.selector=b||"",a},A.isZ=function(a){return a instanceof A.Z},A.init=function(b,d){if(!b)return A.Z();if(F(b))return c(h).ready(b);if(A.isZ(b))return b;var e;if(K(b))e=M(b);else if(I(b))e=[J(b)?c.extend({},b):b],b=null;else if(m.test(b))e=A.fragment(b.trim(),RegExp.$1,d),b=null;else{if(d!==a)return c(d).find(b);e=A.qsa(h,b)}return A.Z(e,b)},c=function(a,b){return A.init(a,b)},c.extend=function(a){var b,c=f.call(arguments,1);return typeof a=="boolean"&&(b=a,a=c.shift()),c.forEach(function(c){T(a,c,b)}),a},A.qsa=function(a,b){var c;return H(a)&&w.test(b)?(c=a.getElementById(RegExp.$1))?[c]:[]:a.nodeType!==1&&a.nodeType!==9?[]:f.call(v.test(b)?a.getElementsByClassName(RegExp.$1):x.test(b)?a.getElementsByTagName(b):a.querySelectorAll(b))},c.contains=function(a,b){return a!==b&&a.contains(b)},c.type=E,c.isFunction=F,c.isWindow=G,c.isArray=K,c.isPlainObject=J,c.isEmptyObject=function(a){var b;for(b in a)return!1;return!0},c.inArray=function(a,b,c){return e.indexOf.call(b,a,c)},c.camelCase=B,c.trim=function(a){return a.trim()},c.uuid=0,c.support={},c.expr={},c.map=function(a,b){var c,d=[],e,f;if(L(a))for(e=0;e=0?b:b+this.length]},toArray:function(){return this.get()},size:function(){return this.length},remove:function(){return this.each(function(){this.parentNode!=null&&this.parentNode.removeChild(this)})},each:function(a){return e.every.call(this,function(b,c){return a.call(b,c,b)!==!1}),this},filter:function(a){return F(a)?this.not(this.not(a)):c(g.call(this,function(b){return A.matches(b,a)}))},add:function(a,b){return c(C(this.concat(c(a,b))))},is:function(a){return this.length>0&&A.matches(this[0],a)},not:function(b){var d=[];if(F(b)&&b.call!==a)this.each(function(a){b.call(this,a)||d.push(this)});else{var e=typeof b=="string"?this.filter(b):L(b)&&F(b.item)?f.call(b):c(b);this.forEach(function(a){e.indexOf(a)<0&&d.push(a)})}return c(d)},has:function(a){return this.filter(function(){return I(a)?c.contains(this,a):c(this).find(a).size()})},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){var a=this[0];return a&&!I(a)?a:c(a)},last:function(){var a=this[this.length-1];return a&&!I(a)?a:c(a)},find:function(a){var b,d=this;return typeof a=="object"?b=c(a).filter(function(){var a=this;return e.some.call(d,function(b){return c.contains(b,a)})}):this.length==1?b=c(A.qsa(this[0],a)):b=this.map(function(){return A.qsa(this,a)}),b},closest:function(a,b){var d=this[0],e=!1;typeof a=="object"&&(e=c(a));while(d&&!(e?e.indexOf(d)>=0:A.matches(d,a)))d=d!==b&&!H(d)&&d.parentNode;return c(d)},parents:function(a){var b=[],d=this;while(d.length>0)d=c.map(d,function(a){if((a=a.parentNode)&&!H(a)&&b.indexOf(a)<0)return b.push(a),a});return U(b,a)},parent:function(a){return U(C(this.pluck("parentNode")),a)},children:function(a){return U(this.map(function(){return S(this)}),a)},contents:function(){return this.map(function(){return f.call(this.childNodes)})},siblings:function(a){return U(this.map(function(a,b){return g.call(S(b.parentNode),function(a){return a!==b})}),a)},empty:function(){return this.each(function(){this.innerHTML=""})},pluck:function(a){return c.map(this,function(b){return b[a]})},show:function(){return this.each(function(){this.style.display=="none"&&(this.style.display=null),k(this,"").getPropertyValue("display")=="none"&&(this.style.display=R(this.nodeName))})},replaceWith:function(a){return this.before(a).remove()},wrap:function(a){var b=F(a);if(this[0]&&!b)var d=c(a).get(0),e=d.parentNode||this.length>1;return this.each(function(f){c(this).wrapAll(b?a.call(this,f):e?d.cloneNode(!0):d)})},wrapAll:function(a){if(this[0]){c(this[0]).before(a=c(a));var b;while((b=a.children()).length)a=b.first();c(a).append(this)}return this},wrapInner:function(a){var b=F(a);return this.each(function(d){var e=c(this),f=e.contents(),g=b?a.call(this,d):a;f.length?f.wrapAll(g):e.append(g)})},unwrap:function(){return this.parent().each(function(){c(this).replaceWith(c(this).children())}),this},clone:function(){return this.map(function(){return this.cloneNode(!0)})},hide:function(){return this.css("display","none")},toggle:function(b){return this.each(function(){var d=c(this);(b===a?d.css("display")=="none":b)?d.show():d.hide()})},prev:function(a){return c(this.pluck("previousElementSibling")).filter(a||"*")},next:function(a){return c(this.pluck("nextElementSibling")).filter(a||"*")},html:function(b){return b===a?this.length>0?this[0].innerHTML:null:this.each(function(a){var d=this.innerHTML;c(this).empty().append(V(this,b,a,d))})},text:function(b){return b===a?this.length>0?this[0].textContent:null:this.each(function(){this.textContent=b})},attr:function(c,d){var e;return typeof c=="string"&&d===a?this.length==0||this[0].nodeType!==1?a:c=="value"&&this[0].nodeName=="INPUT"?this.val():!(e=this[0].getAttribute(c))&&c in this[0]?this[0][c]:e:this.each(function(a){if(this.nodeType!==1)return;if(I(c))for(b in c)W(this,b,c[b]);else W(this,c,V(this,d,a,this.getAttribute(c)))})},removeAttr:function(a){return this.each(function(){this.nodeType===1&&W(this,a)})},prop:function(b,c){return c===a?this[0]&&this[0][b]:this.each(function(a){this[b]=V(this,c,a,this[b])})},data:function(b,c){var d=this.attr("data-"+O(b),c);return d!==null?Y(d):a},val:function(b){return b===a?this[0]&&(this[0].multiple?c(this[0]).find("option").filter(function(a){return this.selected}).pluck("value"):this[0].value):this.each(function(a){this.value=V(this,b,a,this.value)})},offset:function(a){if(a)return this.each(function(b){var d=c(this),e=V(this,a,b,d.offset()),f=d.offsetParent().offset(),g={top:e.top-f.top,left:e.left-f.left};d.css("position")=="static"&&(g.position="relative"),d.css(g)});if(this.length==0)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(a,c){if(arguments.length<2&&typeof a=="string")return this[0]&&(this[0].style[B(a)]||k(this[0],"").getPropertyValue(a));var d="";if(E(a)=="string")!c&&c!==0?this.each(function(){this.style.removeProperty(O(a))}):d=O(a)+":"+Q(a,c);else for(b in a)!a[b]&&a[b]!==0?this.each(function(){this.style.removeProperty(O(b))}):d+=O(b)+":"+Q(b,a[b])+";";return this.each(function(){this.style.cssText+=";"+d})},index:function(a){return a?this.indexOf(c(a)[0]):this.parent().children().indexOf(this[0])},hasClass:function(a){return e.some.call(this,function(a){return this.test(X(a))},P(a))},addClass:function(a){return this.each(function(b){d=[];var e=X(this),f=V(this,a,b,e);f.split(/\s+/g).forEach(function(a){c(this).hasClass(a)||d.push(a)},this),d.length&&X(this,e+(e?" ":"")+d.join(" "))})},removeClass:function(b){return this.each(function(c){if(b===a)return X(this,"");d=X(this),V(this,b,c,d).split(/\s+/g).forEach(function(a){d=d.replace(P(a)," ")}),X(this,d.trim())})},toggleClass:function(b,d){return this.each(function(e){var f=c(this),g=V(this,b,e,X(this));g.split(/\s+/g).forEach(function(b){(d===a?!f.hasClass(b):d)?f.addClass(b):f.removeClass(b)})})},scrollTop:function(){if(!this.length)return;return"scrollTop"in this[0]?this[0].scrollTop:this[0].scrollY},position:function(){if(!this.length)return;var a=this[0],b=this.offsetParent(),d=this.offset(),e=o.test(b[0].nodeName)?{top:0,left:0}:b.offset();return d.top-=parseFloat(c(a).css("margin-top"))||0,d.left-=parseFloat(c(a).css("margin-left"))||0,e.top+=parseFloat(c(b[0]).css("border-top-width"))||0,e.left+=parseFloat(c(b[0]).css("border-left-width"))||0,{top:d.top-e.top,left:d.left-e.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||h.body;while(a&&!o.test(a.nodeName)&&c(a).css("position")=="static")a=a.offsetParent;return a})}},c.fn.detach=c.fn.remove,["width","height"].forEach(function(b){c.fn[b]=function(d){var e,f=this[0],g=b.replace(/./,function(a){return a[0].toUpperCase()});return d===a?G(f)?f["inner"+g]:H(f)?f.documentElement["offset"+g]:(e=this.offset())&&e[b]:this.each(function(a){f=c(this),f.css(b,V(this,d,a,f[b]()))})}}),q.forEach(function(a,b){var d=b%2;c.fn[a]=function(){var a,e=c.map(arguments,function(b){return a=E(b),a=="object"||a=="array"||b==null?b:A.fragment(b)}),f,g=this.length>1;return e.length<1?this:this.each(function(a,h){f=d?h:h.parentNode,h=b==0?h.nextSibling:b==1?h.firstChild:b==2?h:null,e.forEach(function(a){if(g)a=a.cloneNode(!0);else if(!f)return c(a).remove();Z(f.insertBefore(a,h),function(a){a.nodeName!=null&&a.nodeName.toUpperCase()==="SCRIPT"&&(!a.type||a.type==="text/javascript")&&!a.src&&window.eval.call(window,a.innerHTML)})})})},c.fn[d?a+"To":"insert"+(b?"Before":"After")]=function(b){return c(b)[a](this),this}}),A.Z.prototype=c.fn,A.uniq=C,A.deserializeValue=Y,c.zepto=A,c}();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.]+)/);if(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 g(a){return a._zid||(a._zid=d++)}function h(a,b,d,e){b=i(b);if(b.ns)var f=j(b.ns);return(c[g(a)]||[]).filter(function(a){return a&&(!b.e||a.e==b.e)&&(!b.ns||f.test(a.ns))&&(!d||g(a.fn)===g(d))&&(!e||a.sel==e)})}function i(a){var b=(""+a).split(".");return{e:b[0],ns:b.slice(1).sort().join(" ")}}function j(a){return new RegExp("(?:^| )"+a.replace(" "," .* ?")+"(?: |$)")}function k(b,c,d){a.type(b)!="string"?a.each(b,d):b.split(/\s/).forEach(function(a){d(a,c)})}function l(a,b){return a.del&&(a.e=="focus"||a.e=="blur")||!!b}function m(a){return f[a]||a}function n(b,d,e,h,j,n){var o=g(b),p=c[o]||(c[o]=[]);k(d,e,function(c,d){var e=i(c);e.fn=d,e.sel=h,e.e in f&&(d=function(b){var c=b.relatedTarget;if(!c||c!==this&&!a.contains(this,c))return e.fn.apply(this,arguments)}),e.del=j&&j(d,c);var g=e.del||d;e.proxy=function(a){var c=g.apply(b,[a].concat(a.data));return c===!1&&(a.preventDefault(),a.stopPropagation()),c},e.i=p.length,p.push(e),b.addEventListener(m(e.e),e.proxy,l(e,n))})}function o(a,b,d,e,f){var i=g(a);k(b||"",d,function(b,d){h(a,b,d,e).forEach(function(b){delete c[i][b.i],a.removeEventListener(m(b.e),b.proxy,l(b,f))})})}function t(b){var c,d={originalEvent:b};for(c in b)!r.test(c)&&b[c]!==undefined&&(d[c]=b[c]);return a.each(s,function(a,c){d[a]=function(){return this[c]=p,b[a].apply(b,arguments)},d[c]=q}),d}function u(a){if(!("defaultPrevented"in a)){a.defaultPrevented=!1;var b=a.preventDefault;a.preventDefault=function(){this.defaultPrevented=!0,b.call(this)}}}var b=a.zepto.qsa,c={},d=1,e={},f={mouseenter:"mouseover",mouseleave:"mouseout"};e.click=e.mousedown=e.mouseup=e.mousemove="MouseEvents",a.event={add:n,remove:o},a.proxy=function(b,c){if(a.isFunction(b)){var d=function(){return b.apply(c,arguments)};return d._zid=g(b),d}if(typeof c=="string")return a.proxy(b[c],b);throw new TypeError("expected function")},a.fn.bind=function(a,b){return this.each(function(){n(this,a,b)})},a.fn.unbind=function(a,b){return this.each(function(){o(this,a,b)})},a.fn.one=function(a,b){return this.each(function(c,d){n(this,a,b,null,function(a,b){return function(){var c=a.apply(d,arguments);return o(d,b,a),c}})})};var p=function(){return!0},q=function(){return!1},r=/^([A-Z]|layer[XY]$)/,s={preventDefault:"isDefaultPrevented",stopImmediatePropagation:"isImmediatePropagationStopped",stopPropagation:"isPropagationStopped"};a.fn.delegate=function(b,c,d){return this.each(function(e,f){n(f,c,d,b,function(c){return function(d){var e,g=a(d.target).closest(b,f).get(0);if(g)return e=a.extend(t(d),{currentTarget:g,liveFired:f}),c.apply(g,[e].concat([].slice.call(arguments,1)))}})})},a.fn.undelegate=function(a,b,c){return this.each(function(){o(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){if(typeof b=="string"||a.isPlainObject(b))b=a.Event(b);return u(b),b.data=c,this.each(function(){"dispatchEvent"in this&&this.dispatchEvent(b)})},a.fn.triggerHandler=function(b,c){var d,e;return this.each(function(f,g){d=t(typeof b=="string"?a.Event(b):b),d.data=c,d.target=g,a.each(h(g,b.type||b),function(a,b){e=b.proxy(d);if(d.isImmediatePropagationStopped())return!1})}),e},"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){typeof a!="string"&&(b=a,a=b.type);var c=document.createEvent(e[a]||"Events"),d=!0;if(b)for(var f in b)f=="bubbles"?d=!!b[f]:c[f]=b[f];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($){function triggerAndReturn(a,b,c){var d=$.Event(b);return $(a).trigger(d,c),!d.defaultPrevented}function triggerGlobal(a,b,c,d){if(a.global)return triggerAndReturn(b||document,c,d)}function ajaxStart(a){a.global&&$.active++===0&&triggerGlobal(a,null,"ajaxStart")}function ajaxStop(a){a.global&&!--$.active&&triggerGlobal(a,null,"ajaxStop")}function ajaxBeforeSend(a,b){var c=b.context;if(b.beforeSend.call(c,a,b)===!1||triggerGlobal(b,c,"ajaxBeforeSend",[a,b])===!1)return!1;triggerGlobal(b,c,"ajaxSend",[a,b])}function ajaxSuccess(a,b,c){var d=c.context,e="success";c.success.call(d,a,e,b),triggerGlobal(c,d,"ajaxSuccess",[b,c,a]),ajaxComplete(e,b,c)}function ajaxError(a,b,c,d){var e=d.context;d.error.call(e,c,b,a),triggerGlobal(d,e,"ajaxError",[c,d,a]),ajaxComplete(b,c,d)}function ajaxComplete(a,b,c){var d=c.context;c.complete.call(d,b,a),triggerGlobal(c,d,"ajaxComplete",[b,c]),ajaxStop(c)}function empty(){}function mimeToDataType(a){return a&&(a=a.split(";",2)[0]),a&&(a==htmlType?"html":a==jsonType?"json":scriptTypeRE.test(a)?"script":xmlTypeRE.test(a)&&"xml")||"text"}function appendQuery(a,b){return(a+"&"+b).replace(/[&?]{1,2}/,"?")}function serializeData(a){a.processData&&a.data&&$.type(a.data)!="string"&&(a.data=$.param(a.data,a.traditional)),a.data&&(!a.type||a.type.toUpperCase()=="GET")&&(a.url=appendQuery(a.url,a.data))}function parseArguments(a,b,c,d){var e=!$.isFunction(b);return{url:a,data:e?b:undefined,success:e?$.isFunction(c)?c:undefined:b,dataType:e?d||c:c}}function serialize(a,b,c,d){var e,f=$.isArray(b);$.each(b,function(b,g){e=$.type(g),d&&(b=c?d:d+"["+(f?"":b)+"]"),!d&&f?a.add(g.name,g.value):e=="array"||!c&&e=="object"?serialize(a,g,c,b):a.add(b,g)})}var jsonpID=0,document=window.document,key,name,rscript=/