diff --git a/INSTALL b/INSTALL new file mode 100644 index 00000000..fb6c4c25 --- /dev/null +++ b/INSTALL @@ -0,0 +1,67 @@ + +CODER FOR RASPBERRY PI +The easy way... + +Coder for Raspberry Pi is distributed as a Pi SD Card image. +If you want to go the easy route, please check out the +documentation at http://goo.gl/coder + + + +EVERYONE ELSE + +If you want to install Coder on something else, or would +like to install it on an existing Raspberry Pi, you can +manually install it as well. + +BEFORE YOU START: + I recommend you do this as a normal user and not root. + The official pi distro has a "coder" user that Coder runs + under, and ports 80 and 433 are forwarded to 8080/8081. + +MANUAL INSTALL: + +1. You need to have node.js and npm installed + +2. Download Coder from git. + # git clone https://github.com/googlecreativelab/coder.git + +3. Install the basic Coder apps. + # cd coder-apps + # ./install_common.sh ../coder-base + + Optional: Raspberry Pi additions to the code can be installed with: + # ./install_pi.sh ../coder-base + Note that there are a number of additional changes made to the OS. + These additional configurations can be found in the raspbian-addons + directory. See below. + +4. In coder-base run "npm install" to download all the + needed modules. + +5. Edit config.js to your liking. I recommend starting + with the settings in config.js.localhost and running + the localhost server. + +6. Start Coder + # cd coder-base + # node localserver.js + + +If you want to run Coder on an external port, you'll need +to run server.js instead of localserver.js. This requires +a bit of port forwarding setup in iptables. Look in +the raspbian-addons directory to see the customizations that +were made to the stock raspbian distro. + +The raspberry pi version of Coder has some other +tweaks that allow you to change your wifi settings +and keep your Pi password in sync with your Coder password. +There's some convoluted system configuration involved, which is +probably why you'd want to start with the Coder disk image, +but the modified apps are available by running ./install_pi.sh +after step 3. Modifications to the stock raspbian configuration +can be found in raspbian-addons. + + + diff --git a/README.md b/README.md index 55cb19f9..4298e1e6 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,10 @@ http://goo.gl/coder ### What You'll Find Here #### coder-base -The Coder node.js server and applications that come pre-installed +The Coder node.js server and application files + +#### coder-apps +The Coder applications that come pre-installed in the Coder distribution #### raspbian-addons Modifications and additions to the stock raspian configuration and init structure diff --git a/coder-apps/archive_app.sh b/coder-apps/archive_app.sh new file mode 100755 index 00000000..0012ca5a --- /dev/null +++ b/coder-apps/archive_app.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +## +## Copies an application from the coder-base working directory to +## the coder-apps directory. +## +## sh archive_app appname base_path apps_path +## +## Eg. +## sh archive_app hello_coder ../coder-base/ ./common/ + +if [ $# != 3 ] + then + echo -e "\nUse:\narchive_app appname base_path apps_path\n" + exit +fi + +app=$1 +base=$2 +dest=$3 + +mkdir "$dest/$app" +mkdir "$dest/$app/app" +mkdir "$dest/$app/static" +mkdir "$dest/$app/static/js" +mkdir "$dest/$app/static/css" +mkdir "$dest/$app/static/media" +mkdir "$dest/$app/views" +touch "$dest/$app/static/media/.gitignore" + +cp $base/apps/$app/* $dest/$app/app/ +cp $base/views/apps/$app/* $dest/$app/views/ +cp $base/static/apps/$app/js/* $dest/$app/static/js/ +cp $base/static/apps/$app/css/* $dest/$app/static/css/ +cp $base/static/apps/$app/media/* $dest/$app/static/media/ diff --git a/coder-apps/common/auth/app/app.js b/coder-apps/common/auth/app/app.js new file mode 100644 index 00000000..4f4a42f5 --- /dev/null +++ b/coder-apps/common/auth/app/app.js @@ -0,0 +1,629 @@ +/** + * Coder for Raspberry Pi + * A simple platform for experimenting with web stuff. + * http://goo.gl/coder + * + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var mustache = require('mustache'); +var util = require('util'); +var fs = require('fs'); +var bcrypt = require('bcrypt-nodejs'); + +//stores cache of password hash and device name +var device_settings = { + password_hash: '', + device_name: '', + hostname: '', + coder_owner: '', + coder_color: '#3e3e3e' +}; + + +exports.settings={}; +//These are dynamically updated by the runtime +//settings.appname - the app id (folder) where your app is installed +//settings.viewpath - prefix to where your view html files are located +//settings.staticurl - base url path to static assets /static/apps/appname +//settings.appurl - base url path to this app /app/appname +//settings.device_name - name the user gave to their coder "Susie's Coder" + + +exports.get_routes = [ + { path:'/', handler:'index_handler'}, + { path:'/login', handler:'login_handler'}, + { path:'/logout', handler:'logout_handler'}, + { path:'/configure', handler:'configure_handler'}, + { path:'/addpassword', handler:'addpassword_handler'}, + { path:'/changepassword', handler:'changepassword_handler'}, + { path: '/api/devicename/get', handler: 'api_devicename_get_handler' }, + { path: '/api/codercolor/get', handler: 'api_codercolor_get_handler' }, + { path: '/api/coderowner/get', handler: 'api_coderowner_get_handler' } +]; + + +exports.post_routes = [ + { path: '/api/login', handler: 'api_login_handler' }, + { path: '/api/logout', handler: 'api_logout_handler' }, + { path: '/api/devicename/set', handler: 'api_devicename_set_handler' }, + { path: '/api/codercolor/set', handler: 'api_codercolor_set_handler' }, + { path: '/api/coderowner/set', handler: 'api_coderowner_set_handler' }, + { path: '/api/addpassword', handler: 'api_addpassword_handler' }, + { path: '/api/changepassword', handler: 'api_changepassword_handler' } +]; + +exports.on_destroy = function() { +}; + + +exports.isAuthenticated = function( req ) { + if ( typeof req.session !== 'undefined' && typeof req.session.authenticated !== 'undefined' ) { + return req.session.authenticated === true; + } + return false; +}; + +exports.isConfigured = function() { + if ( typeof device_settings.device_name !== 'undefined' && device_settings.device_name !== '' && + typeof device_settings.hostname !== 'undefined' && device_settings.hostname !== '' ) { + return true; + } else { + return false; + } +}; + +exports.hasPassword = function() { + if ( typeof device_settings.password_hash !== 'undefined' && device_settings.password_hash !== '' ) { + return true; + } else { + return false; + } +}; + +exports.getDeviceName = function() { + return device_settings.device_name; +}; +exports.getCoderOwner = function() { + return device_settings.coder_owner; +}; +exports.getCoderColor = function() { + return device_settings.coder_color; +}; + +exports.authenticate = function( req, password ) { + + var authenticated = bcrypt.compareSync( password, device_settings.password_hash ); + if ( authenticated ) { + req.session.authenticated = true; + } + + return authenticated; +}; + +exports.logout = function( req ) { + + req.session.authenticated = false; +}; + + +exports.index_handler = function( req, res ) { + + var firstuse = "?firstuse"; + if ( typeof( req.param('firstuse') ) === 'undefined' ) { + firstuse = ""; + } + + if ( !exports.isConfigured() ) { + res.redirect('/app/auth/configure?firstuse'); + } else if ( !exports.hasPassword() ) { + res.redirect('/app/auth/addpassword?firstuse'); + } else if ( !exports.isAuthenticated(req) ) { + res.redirect('/app/auth/login' + firstuse); + } else { + res.redirect('/app/coder' + firstuse); + } +}; + +exports.addpassword_handler = function( req, res ) { + var tmplvars = {}; + tmplvars['static_url'] = exports.settings.staticurl; + tmplvars['app_name'] = exports.settings.appname; + tmplvars['app_url'] = exports.settings.appurl; + tmplvars['device_name'] = exports.settings.device_name; + tmplvars['page_mode'] = "addpassword"; + + //only allow this step if they have not yet set a password + if ( !exports.hasPassword() ) { + res.render( exports.settings.viewpath + '/index', tmplvars ); + } else { + res.redirect('/app/auth/login'); + } +}; + +exports.changepassword_handler = function( req, res ) { + var tmplvars = {}; + tmplvars['static_url'] = exports.settings.staticurl; + tmplvars['app_name'] = exports.settings.appname; + tmplvars['app_url'] = exports.settings.appurl; + tmplvars['device_name'] = exports.settings.device_name; + tmplvars['page_mode'] = "changepassword"; + + //only allow this step if they are authenticated + if ( exports.isAuthenticated(req) ) { + res.render( exports.settings.viewpath + '/index', tmplvars ); + } else { + res.redirect('/app/auth/login'); + } +}; + +exports.configure_handler = function( req, res ) { + var tmplvars = {}; + tmplvars['static_url'] = exports.settings.staticurl; + tmplvars['app_name'] = exports.settings.appname; + tmplvars['app_url'] = exports.settings.appurl; + tmplvars['device_name'] = exports.settings.device_name; + tmplvars['page_mode'] = "configure"; + + //only allow this step if they are authenticated or have not yet set a password + if ( exports.isAuthenticated(req) || !exports.hasPassword() ) { + res.render( exports.settings.viewpath + '/index', tmplvars ); + } else { + res.redirect('/app/auth/login'); + } +}; + +exports.api_devicename_get_handler = function( req, res ) { + res.json({ + device_name: exports.getDeviceName() + }); +}; +exports.api_codercolor_get_handler = function( req, res ) { + res.json({ + coder_color: exports.getCoderColor() + }); +}; +exports.api_coderowner_get_handler = function( req, res ) { + //only allow this step if they are authenticated or have not yet set a password + if ( !exports.isAuthenticated(req) && exports.hasPassword() ) { + res.json({ + status: "error", + error: "not authenticated" + }); + return; + } + res.json({ + coder_owner: exports.getCoderOwner() + }); +}; + +exports.api_devicename_set_handler = function( req, res ) { + + //only allow this step if they are authenticated or have not yet set a password + if ( !exports.isAuthenticated(req) && exports.hasPassword() ) { + res.json({ + status: "error", + error: "not authenticated" + }); + return; + } + + var devicename = req.param('device_name'); + if ( !devicename || devicename === "" || !isValidDeviceName( devicename ) ) { + res.json({ + status: 'error', + error: "invalid device name" + }); + return; + } + + device_settings.device_name = devicename; + device_settings.hostname = hostnameFromDeviceName( devicename ); + + err = saveDeviceSettings(); + + if ( !err ) { + res.json({ + status: "success", + device_name: device_settings.device_name, + hostname: device_settings.hostname + }); + } else { + res.json({ + status: "error", + error: "could not save device settings" + }); + } + +}; + + +exports.api_coderowner_set_handler = function( req, res ) { + + //only allow this step if they are authenticated or have not yet set a password + if ( !exports.isAuthenticated(req) && exports.hasPassword() ) { + res.json({ + status: "error", + error: "not authenticated" + }); + return; + } + + var owner = req.param('coder_owner'); + if ( typeof owner === 'undefined' ) { + res.json({ + status: 'error', + error: "invalid owner name" + }); + return; + } + + device_settings.coder_owner = owner; + + err = saveDeviceSettings(); + + if ( !err ) { + res.json({ + status: "success", + coder_owner: device_settings.coder_owner + }); + } else { + res.json({ + status: "error", + error: "could not save device settings" + }); + } + +}; + +exports.api_codercolor_set_handler = function( req, res ) { + + //only allow this step if they are authenticated or have not yet set a password + if ( !exports.isAuthenticated(req) && exports.hasPassword() ) { + res.json({ + status: "error", + error: "not authenticated" + }); + return; + } + + var color = req.param('coder_color'); + if ( typeof color === 'undefined' || !isValidColor( color ) ) { + res.json({ + status: 'error', + error: "invalid color" + }); + return; + } + + device_settings.coder_color = color; + + err = saveDeviceSettings(); + + if ( !err ) { + res.json({ + status: "success", + coder_color: device_settings.coder_color + }); + } else { + res.json({ + status: "error", + error: "could not save device settings" + }); + } + +}; + +exports.api_addpassword_handler = function( req, res ) { + + //only allow this step if they have not yet set a password + if ( exports.hasPassword() ) { + res.json({ + status: "error", + error: "not authenticated" + }); + return; + } + + var pass = req.param('password'); + if ( !pass || pass === "" || !isValidPassword( pass ) ) { + res.json({ + status: 'error', + error: getPasswordProblem( pass ) + }); + return; + } + + var spawn = require('child_process').spawn; + var err=0; + //device_settings.device_name = devicename; + var erroutput = ""; + var output = ""; + //var setpipass = process.cwd() + '/sudo_scripts/setpipass'; + //var setpass = spawn( '/usr/bin/sudo', [setpipass] ); + //setpass.stdout.on( 'data', function( d ) { + // output += d; + //}); + //setpass.stderr.on( 'data', function( d ) { + // erroutput += d; + //}); + + //setpass.addListener( 'exit', function( code, signal ) { + var completed = function( code, signal ) { + err = code; + + + if ( err ) { + res.json({ + status: "error", + error: erroutput + }); + return; + } + + //TODO - Load hashed password + var s = bcrypt.genSaltSync(10); + var h = bcrypt.hashSync( pass, s ); + util.log("PASSWORD INITIALIZED"); + device_settings.password_hash = h; + err = saveDeviceSettings(); + + if ( !err ) { + res.json({ + status: "success" + }); + } else { + res.json({ + status: "error", + error: "Could not save device settings." + }); + } + + }; + + completed(); + + //setpass.stdin.write(pass + '\n'); + //setpass.stdin.write(pass + '\n'); + //setpass.stdin.end(); + +}; + + + +exports.api_changepassword_handler = function( req, res ) { + + //only allow this step if they are authenticated + if ( !exports.isAuthenticated(req) ) { + res.json({ + status: "error", + error: "not authenticated" + }); + return; + } + + var oldpass = req.param('oldpassword'); + var pass = req.param('password'); + + //Make sure old pass is set and matches + if ( typeof oldpass === 'undefined' || oldpass === "" + || !bcrypt.compareSync( oldpass, device_settings.password_hash ) ) { + res.json({ + status: 'error', + error: "old password was incorrect" + }); + return; + } + + if ( !pass || pass === "" || !isValidPassword( pass ) ) { + res.json({ + status: 'error', + error: getPasswordProblem( pass ) + }); + return; + } + + var spawn = require('child_process').spawn; + var err=0; + //device_settings.device_name = devicename; + var erroutput = ""; + var output = ""; + var setpipass = process.cwd() + '/sudo_scripts/setpipass'; + var setpass = spawn( '/usr/bin/sudo', [setpipass] ); + setpass.stdout.on( 'data', function( d ) { + output += d; + }); + setpass.stderr.on( 'data', function( d ) { + erroutput += d; + }); + + setpass.addListener( 'exit', function( code, signal ) { + err = code; + + + if ( err ) { + res.json({ + status: "error", + error: erroutput + }); + return; + } + + //TODO - Load hashed password + var s = bcrypt.genSaltSync(10); + var h = bcrypt.hashSync( pass, s ); + util.log("PASSWORD INITIALIZED"); + device_settings.password_hash = h; + err = saveDeviceSettings(); + + if ( !err ) { + res.json({ + status: "success" + }); + } else { + res.json({ + status: "error", + error: "Could not save device settings." + }); + } + + }); + setpass.stdin.write(pass + '\n'); + setpass.stdin.write(pass + '\n'); + setpass.stdin.end(); + +}; + + +exports.login_handler = function( req, res ) { + var tmplvars = {}; + tmplvars['static_url'] = exports.settings.staticurl; + tmplvars['app_name'] = exports.settings.appname; + tmplvars['app_url'] = exports.settings.appurl; + tmplvars['device_name'] = exports.settings.device_name; + tmplvars['page_mode'] = "login"; + + + //TODO - should this log you out automatically? + req.session.authenticated = false; + res.render( exports.settings.viewpath + '/index', tmplvars ); +}; + +exports.logout_handler = function( req, res ) { + var tmplvars = {}; + tmplvars['static_url'] = exports.settings.staticurl; + tmplvars['app_name'] = exports.settings.appname; + tmplvars['app_url'] = exports.settings.appurl; + tmplvars['device_name'] = exports.settings.device_name; + tmplvars['page_mode'] = "logout"; + + req.session.authenticated = false; + res.render( exports.settings.viewpath + '/index', tmplvars ); +}; + +exports.api_login_handler = function( req, res ) { + if ( typeof req.body.password !== 'undefined' && req.body.password !== "" ) { + var authenticated = exports.authenticate( req, req.body.password ); + if ( authenticated === true ) { + res.json( { status: 'success'} ); + return; + } + } + res.json( { + status: 'error', + error: 'invalid password' + } ); +}; +exports.api_logout_handler = function( req, res ) { + req.session.authenticated = false; + + res.json( { status: 'success'} ); +}; + +var saveDeviceSettings = function() { + err = fs.writeFileSync( process.cwd() + "/device.json", JSON.stringify(device_settings, null, 4), 'utf8' ); + fs.chmodSync(process.cwd() + '/device.json', '600'); + return err; +}; + +var reloadDeviceSettings = function() { + var settings = { + password_hash: '', + device_name: '', + hostname: '', + coder_owner: '', + coder_color: '' + }; + + var loadedsettings = JSON.parse(fs.readFileSync( process.cwd() + "/device.json", 'utf-8' )); + settings.password_hash = ( typeof loadedsettings.password_hash !== 'undefined' && loadedsettings.password_hash !== '' ) ? loadedsettings.password_hash : settings.password_hash; + settings.device_name = ( typeof loadedsettings.device_name !== 'undefined' && loadedsettings.device_name !== '' ) ? loadedsettings.device_name : settings.device_name; + settings.hostname = ( typeof loadedsettings.hostname !== 'undefined' && loadedsettings.hostname !== '' ) ? loadedsettings.hostname : settings.hostname; + settings.coder_owner = ( typeof loadedsettings.coder_owner !== 'undefined' && loadedsettings.coder_owner !== '' ) ? loadedsettings.coder_owner : settings.coder_owner; + settings.coder_color = ( typeof loadedsettings.coder_color !== 'undefined' && loadedsettings.coder_color !== '' ) ? loadedsettings.coder_color : settings.coder_color; + + device_settings = settings; +} +reloadDeviceSettings(); + + +var isValidDeviceName = function( name ) { + if ( !name || name === '' ) { + return false; + } + //starts with an ascii word char. can contain word char's spaces and ' + if ( !name.match(/^[a-zA-Z0-9][\w ']*$/) ) { + return false; + } + //ends in an ascii word char + if ( !name.match(/[a-zA-Z0-9]$/) ) { + return false; + } + return true; +}; +var hostnameFromDeviceName = function( name ) { + var hostname = name; + hostname = hostname.toLowerCase(); + hostname = hostname.replace(/[^a-z0-9\- ]/g, ''); + hostname = hostname.replace(/[\- ]+/g,'-'); + return hostname; +}; + +var getPasswordProblem = function( pass ) { + if ( !pass || pass === '' ) { + return "the password is empty"; + } + if ( pass.length < 6 ) { + return "the password should contain at least 6 characters"; + } + if ( !pass.match(/[a-z]/) || + !pass.match(/[A-Z0-9\-\_\.\,\;\:\'\"\[\]\{\}\!\@\#\$\%\^\&\*\(\)\\].*[A-Z0-9\-\_\.\,\;\:\'\"\[\]\{\}\!\@\#\$\%\^\&\*\(\)\\]/) ) { + return "your password must contain a lower case letter and at least two upper case letters or numbers"; + } +}; + +var isValidPassword = function( pass ) { + if ( !pass || pass === '' ) { + return false; + } + //at least 6 characters + if ( pass.length < 6 ) { + return false; + } + //contains lower case + if ( !pass.match(/[a-z]/) ) { + return false; + } + //contains two upper case or numbers + if ( !pass.match(/[A-Z0-9\-\_\.\,\;\:\'\"\[\]\{\}\!\@\#\$\%\^\&\*\(\)\\].*[A-Z0-9\-\_\.\,\;\:\'\"\[\]\{\}\!\@\#\$\%\^\&\*\(\)\\]/) ) { + return false; + } + return true; +}; + +var isValidColor = function( color ) { + if ( !color || color === '' ) { + return false; + } + color = color.toLowerCase(); + if ( !color.match(/^\#[a-f0-9]{6}$/) ) { + return false; + } + return true; +} + + + + diff --git a/coder-base/apps/auth/meta.json b/coder-apps/common/auth/app/meta.json similarity index 100% rename from coder-base/apps/auth/meta.json rename to coder-apps/common/auth/app/meta.json diff --git a/coder-base/static/apps/auth/css/index.css b/coder-apps/common/auth/static/css/index.css similarity index 100% rename from coder-base/static/apps/auth/css/index.css rename to coder-apps/common/auth/static/css/index.css diff --git a/coder-base/static/apps/auth/js/index.js b/coder-apps/common/auth/static/js/index.js similarity index 100% rename from coder-base/static/apps/auth/js/index.js rename to coder-apps/common/auth/static/js/index.js diff --git a/coder-base/views/apps/coderlib/index.html b/coder-apps/common/auth/static/media/.gitignore similarity index 100% rename from coder-base/views/apps/coderlib/index.html rename to coder-apps/common/auth/static/media/.gitignore diff --git a/coder-apps/common/auth/views/index.html b/coder-apps/common/auth/views/index.html new file mode 100644 index 00000000..d2da0a2a --- /dev/null +++ b/coder-apps/common/auth/views/index.html @@ -0,0 +1,108 @@ + + + + Coder + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + + + + + + + + + + +
+
+ + + + + + diff --git a/coder-base/apps/boilerplate/app.js b/coder-apps/common/boilerplate/app/app.js similarity index 100% rename from coder-base/apps/boilerplate/app.js rename to coder-apps/common/boilerplate/app/app.js diff --git a/coder-base/apps/boilerplate/meta.json b/coder-apps/common/boilerplate/app/meta.json similarity index 100% rename from coder-base/apps/boilerplate/meta.json rename to coder-apps/common/boilerplate/app/meta.json diff --git a/coder-base/static/apps/boilerplate/css/index.css b/coder-apps/common/boilerplate/static/css/index.css similarity index 100% rename from coder-base/static/apps/boilerplate/css/index.css rename to coder-apps/common/boilerplate/static/css/index.css diff --git a/coder-base/static/apps/boilerplate/js/index.js b/coder-apps/common/boilerplate/static/js/index.js similarity index 100% rename from coder-base/static/apps/boilerplate/js/index.js rename to coder-apps/common/boilerplate/static/js/index.js diff --git a/coder-apps/common/boilerplate/static/media/.gitignore b/coder-apps/common/boilerplate/static/media/.gitignore new file mode 100644 index 00000000..e69de29b diff --git a/coder-base/views/apps/boilerplate/index.html b/coder-apps/common/boilerplate/views/index.html similarity index 100% rename from coder-base/views/apps/boilerplate/index.html rename to coder-apps/common/boilerplate/views/index.html diff --git a/coder-base/apps/coder/app.js b/coder-apps/common/coder/app/app.js similarity index 100% rename from coder-base/apps/coder/app.js rename to coder-apps/common/coder/app/app.js diff --git a/coder-base/apps/coder/meta.json b/coder-apps/common/coder/app/meta.json similarity index 100% rename from coder-base/apps/coder/meta.json rename to coder-apps/common/coder/app/meta.json diff --git a/coder-base/static/apps/coder/css/index.css b/coder-apps/common/coder/static/css/index.css similarity index 96% rename from coder-base/static/apps/coder/css/index.css rename to coder-apps/common/coder/static/css/index.css index 67b032d4..80fecd33 100644 --- a/coder-base/static/apps/coder/css/index.css +++ b/coder-apps/common/coder/static/css/index.css @@ -50,6 +50,12 @@ top: 0px; right: 0px; padding: 12px; + opacity: 0.5; + font-weight: bold; +} +.appitem .editbutton:hover { + opacity: 1; + cursor: pointer; } #addapp_button { @@ -67,14 +73,14 @@ margin-left: -20px; margin-top: -20px; background-color: transparent; - border-radius: 4px; + /*border-radius: 4px;*/ position: absolute; background-image: url(http://github.com/static/common/media/coder_icons.png); background-position: -167px 0px; opacity: 0.8; } #addapp_button:hover #addapp_icon { - background-color: rgba(255,255,255,0.1); + /*background-color: rgba(255,255,255,0.1);*/ opacity: 1; cursor: pointer; } @@ -194,6 +200,10 @@ margin: 0; border: 0; background-color: #f00; + cursor: pointer; +} +#import_file:hover { + cursor: pointer; } @@ -226,7 +236,7 @@ } .settingsEnabled #settings_button { background-color: rgba( 255,255,255,.2 ); - border-color: #ffffff; + border-color: rgba( 255,255,255,.4 ); } diff --git a/coder-apps/common/coder/static/js/index.js b/coder-apps/common/coder/static/js/index.js new file mode 100644 index 00000000..dc9cd3b9 --- /dev/null +++ b/coder-apps/common/coder/static/js/index.js @@ -0,0 +1,454 @@ +/** + * Coder for Raspberry Pi + * A simple platform for experimenting with web stuff. + * http://goo.gl/coder + * + * Copyright 2013 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var metadata; + +$(document).ready( function() { + + Coder.listApps( buildAppList ); + + $('#addapp_button').click( function(e){ + $(this).hide(); + $("#createform").show(); + }); + $('#createform .cancel').click( function(e){ + $("#createform").hide(); + $("#addapp_button").show(); + }); + $('#createform .submit').click( createAppClicked ); + + $('#import_file').on('change', handleFileImport ); + + $('#createform .formfield.textinput .label').click( focusTextInput ); + $('#createform .formfield.textinput input').click( focusTextInput ); + $('#createform .formfield.textinput input').focus( hideTextLabel ); + $('#createform .formfield.textinput input').blur( onBlurTextInput ); + + $("#createform .colorchit").click( selectAppColor ); + activateCurrentColor(); + + + $("#settings_button").click( toggleSettings ); + $("#settingscontainer .changepass").click( function() { + window.location.href="http://github.com/app/auth/changepassword"; + }); + + $("#settingscontainer .colorchit").click( selectCoderColor ); + activateCurrentCoderColor(); + updateSettingsData(); + $("#settingscontainer input").on('change', checkChangedSettings ); + $("#settingscontainer input").on('keydown', function() { setTimeout( checkChangedSettings, 0); } ); + $("#settingscontainer .cancel").click( revertSettings ); + $("#settingscontainer .save").click( saveSettings ); + + $("#settingscontainer .logout").click( function() { + window.location.href="http://github.com/app/auth/logout"; + }); + + if ( typeof getParams['firstuse'] !== 'undefined' ) { + setTimeout( function(){ + buildIntroduction(); + }, 400 ); + } else { + $('#introduction').css('display','none'); + } +}); + + +var buildIntroduction = function() { + $('#introduction').css({ + 'display': 'none', + 'visibility': 'visible' + }).fadeIn( 'slow', function() { + setTimeout( function() { + $('#myapps_tip').css({'visibility':'visible'}).hide().fadeIn(); + }, 1000); + setTimeout( function() { + $('#newapp_tip').css({'visibility':'visible'}).hide().fadeIn(); + }, 2000); + setTimeout( function() { + $('#settings_tip').css({'visibility':'visible'}).hide().fadeIn(); + }, 3000); + }); + $('.gotit').click( function() { + $('#introduction').fadeOut(function() { + $(this).hide(); + }); + }); +}; + + + + + +var revertSettings = function() { + activateCurrentCoderColor(); + updateSettingsData(); + checkChangedSettings(); +}; + +var updateSettingsData = function( ) { + $('#coder_name').val( device_name ); + $('#coder_ownername').val( coder_owner ); +}; + +var hideTextLabel = function() { + $(this).parent().find('.label').hide(); +}; +var focusTextInput = function() { + $(this).parent().find('input').focus(); +}; +var onBlurTextInput = function() { + if ( $(this).val() == "" ) { + $(this).parent().find('.label').show(); + } +}; +var thefile = null; +var handleFileImport = function( ev ) { + var files = ev.target.files; + + if ( files && files.length > 0 ) { + var importfile = files[0]; + + //console.log( importfile ); + if (!importfile.type.match('application/zip') && !importfile.name.match(/\.zip$/i)) { + alert('This doesn\'t appear to be a Coder project zip file'); + return false; + } + thefile = importfile; + + var fdata = new FormData(); + fdata.append( 'import_file', thefile ); + fdata.append( 'test', 'foo' ); + + $.ajax({ + url: '/app/coder/api/app/import', + type: 'POST', + contentType: false, + processData: false, + cache: false, + data: fdata, + success: function( data ) { + //console.log('upload returned'); + //console.log(data); + if ( data.status === 'success' ) { + var newappid = data.appname; + window.location.href = '/app/editor/edit/' + encodeURIComponent(newappid); + } else if ( typeof data.error !== 'undefined' ) { + alert( data.error ); + } + } + }); + + + /* + var reader = new FileReader(); + // Closure to capture the file information. + reader.onload = (function(theFile) { + return function(e) { + // Render thumbnail. + var span = document.createElement('span'); + span.innerHTML = [''].join(''); + document.getElementById('list').insertBefore(span, null); + }; + })(f); + + reader.readAsDataURL(f); + */ + } +}; + + +var settingson = false; +var toggleSettings = function() { + settingson = !settingson; + enableSettings( settingson ); +}; +var enableSettings = function( on ) { + settingson = on; + if ( settingson ) { + $("body").addClass('settingsEnabled'); + } else { + $("body").removeClass('settingsEnabled'); + } +}; + + +var activateCurrentCoderColor = function() { + var current = coder_color; + $("#coder_nav").css('background-color', current); + $("#settingscontainer .colorchit").each( function() { + $this = $(this); + $this.removeClass('active'); + + if ( rgb2hex($this.css('background-color')) === current ) { + $this.addClass('active'); + } + }); + +}; + +var selectCoderColor = function() { + $this = $(this); + $("#settingscontainer .colorchit").removeClass('active'); + $this.addClass('active'); + checkChangedSettings(); +}; + +var device_changed = false; +var owner_changed = false; +var color_changed = false; +var checkChangedSettings = function() { + var changed = false; + device_changed = false; + owner_changed = false; + color_changed = false; + + if ( $('#coder_name').val() !== device_name ) { + changed = device_changed = true; + } + if ( $('#coder_ownername').val() !== coder_owner ) { + changed = owner_changed = true; + } + var $selectedcolor = $("#settingscontainer .colorchit.active").first(); + if ( $selectedcolor.get(0) && rgb2hex($selectedcolor.css('background-color')) !== coder_color.toLowerCase() ) { + changed = color_changed = true; + } + + if ( changed ) { + $('#settingscontainer').addClass('changed'); + } else { + $('#settingscontainer').removeClass('changed'); + } +}; + +var saveSettings = function() { + + var saveDeviceName = function(callback) { + if ( !device_changed ) { + callback(); + return; + } + $.post('/app/auth/api/devicename/set', + { 'device_name': $('#coder_name').val() }, + function(d) { + //console.log( d ); + if ( d.status === 'success' ) { + device_name = d.device_name; + $("#coder_logo").text( device_name ); + } + callback(); + } + ); + }; + + var saveOwnerName = function(callback) { + if ( !owner_changed ) { + callback(); + return; + } + $.post('/app/auth/api/coderowner/set', + { 'coder_owner': $('#coder_ownername').val() }, + function(d) { + //console.log( d ); + if ( d.status === 'success' ) { + coder_owner = d.coder_owner; + } + callback(); + } + ); + }; + + var saveCoderColor = function(callback) { + if ( !color_changed ) { + callback(); + return; + } + var $selectedcolor = $("#settingscontainer .colorchit.active").first(); + if ( !$selectedcolor.get(0) ) { + callback(); + return; + } + var hexcolor = rgb2hex($selectedcolor.css('background-color')); + + + $.post('/app/auth/api/codercolor/set', + { 'coder_color': hexcolor }, + function(d) { + //console.log( d ); + if ( d.status === 'success' ) { + coder_color = d.coder_color; + $("#coder_nav").css('background-color', coder_color); + } + callback(); + } + ); + }; + + saveDeviceName(function() { + saveOwnerName(function() { + saveCoderColor(function() { + checkChangedSettings(); + }); + }); + }); + +}; + +var buildAppList = function(apps){ + + + //get the app color from our own app (appname is set globally in template) + metadata = apps[appname].metadata; + $('.userbgcolor').css('background-color', metadata.color); + + var $apptmpl = $('#appitem_template').clone(); + $apptmpl.attr('id', '').css('display',''); + + var launchApp = function( appname ) { + return function(e) { + e.preventDefault(); + e.stopPropagation(); + window.location.href = '/app/' + encodeURIComponent(appname); + }; + }; + var editApp = function( appname ) { + return function(e) { + e.preventDefault(); + e.stopPropagation(); + window.location.href = '/app/editor/edit/' + encodeURIComponent(appname); + }; + }; + + + + //Sort the apps by more recently modified + var sortedapps = []; + for ( var k in apps ) { + sortedapps.push( apps[k] ); + } + sortedapps.sort( function(a,b) { + if ( a.ctime < b.ctime ) { + return 1; + } else if ( b.ctime < a.ctime ) { + return -1; + } else { + return 0; + } + }); + + for ( var x=0; x