started refacrtoring for sandboxed iframe

This commit is contained in:
Tankred Hase 2013-06-04 21:36:13 +02:00
parent b580506465
commit 71d6d6c799
15 changed files with 564 additions and 50 deletions

45
.jshintrc Normal file
View File

@ -0,0 +1,45 @@
{
"strict": true,
"globalstrict": true,
"jquery": true,
"node": true,
"browser": true,
"camelcase": true,
"nonew": true,
"curly": true,
"eqeqeq": true,
"immed": true,
"newcap": true,
"regexp": true,
"evil": true,
"eqnull": true,
"expr": true,
"trailing": true,
"undef": true,
"unused": true,
"predef": [
"self",
"console",
"importScripts",
"cryptoLib",
"Backbone",
"forge",
"uuid",
"Lawnchair",
"_",
"process",
"describe",
"it",
"chai",
"ok",
"equal",
"deepEqual",
"start",
"chrome"
],
"globals": {
"app": true
}
}

View File

@ -42,15 +42,19 @@ module.exports = function(grunt) {
},
jshint: {
all: ['Gruntfile.js', 'src/js/**/*.js']
all: ['Gruntfile.js', 'src/js/**/*.js'],
options: {
jshintrc: '.jshintrc'
}
},
qunit: {
all: {
options: {
urls: [
'http://localhost:<%= connect.test.options.port %>/test/unit/index.html',
'http://localhost:<%= connect.test.options.port %>/test/integration/index.html']
'http://localhost:<%= connect.test.options.port %>/test/unit/index.html',
'http://localhost:<%= connect.test.options.port %>/test/integration/index.html'
]
}
}
}
@ -63,7 +67,7 @@ module.exports = function(grunt) {
// Default task(s).
grunt.registerTask('dev', ['connect:dev']);
grunt.registerTask('test', ['jshint', 'connect:test', 'qunit']);
grunt.registerTask('test', ['connect:test', 'qunit']);
grunt.registerTask('prod', ['connect:prod']);
};

10
src/background.js Normal file
View File

@ -0,0 +1,10 @@
'use strict';
chrome.app.runtime.onLaunched.addListener(function() {
chrome.app.window.create('index.html', {
'bounds': {
'width': 805,
'height': 620
}
});
});

View File

@ -41,4 +41,14 @@ textarea.ui-input-text {
max-width: 400px;
margin-left: auto;
margin-right: auto;
}
div#sandboxDiv iframe {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
height: 100%;
width: 100%;
}

View File

@ -5,7 +5,6 @@
<meta charset="utf-8">
<title>Mail</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/jquery.mobile-1.2.0.min.css"/>
<link rel="stylesheet" href="css/styles.css"/>
<!-- The Scripts -->
@ -13,9 +12,7 @@
<script src="lib/jquery-1.8.2.min.js"></script>
<script src="lib/underscore-1.4.4.min.js"></script>
<script src="lib/backbone-1.0.0.min.js"></script>
<script src="js/jqm-config.js"></script>
<script src="lib/jquery.mobile-1.2.0.min.js"></script>
<script src="lib/lawnchair/lawnchair-git.min.js"></script>
<script src="lib/lawnchair/lawnchair-git.js"></script>
<script src="lib/lawnchair/lawnchair-adapter-webkit-sqlite-git.js"></script>
<script src="lib/lawnchair/lawnchair-adapter-indexed-db-git.js"></script>
@ -41,18 +38,14 @@
<script src="js/dao/cloudstorage-dao.js"></script>
<script src="js/dao/keychain-dao.js"></script>
<script src="js/dao/email-dao.js"></script>
<script src="js/view/login-view.js"></script>
<script src="js/view/compose-view.js"></script>
<script src="js/view/messagelist-view.js"></script>
<script src="js/view/messagelistitem-view.js"></script>
<script src="js/view/folderlist-view.js"></script>
<script src="js/view/read-view.js"></script>
<script src="js/app-router.js"></script>
<script src="js/loader.js"></script>
<script src="js/window-loader.js"></script>
</head>
<body></body>
<body>
<div id="sandboxDiv">
<iframe id="sandboxFrame" src="sandbox.html" sandbox="allow-scripts" frameborder="0"></iframe>
</div>
</body>
</html>

View File

@ -16,13 +16,7 @@
login: function() {
// init email dao and dependencies
var util = new cryptoLib.Util(window, uuid);
var crypto = new app.crypto.Crypto(window, util);
var cloudstorage = new app.dao.CloudStorage(window, $);
var jsonDao = new app.dao.LawnchairDAO(Lawnchair);
var devicestorage = new app.dao.DeviceStorage(util, crypto, jsonDao, null);
var keychain = new app.dao.KeychainDAO(jsonDao, cloudstorage);
this.emailDao = new app.dao.EmailDAO(jsonDao, crypto, devicestorage, cloudstorage, util, keychain);
this.emailDao = null;
var loginView = new app.view.LoginView({
dao: this.emailDao

37
src/js/sandbox-loader.js Normal file
View File

@ -0,0 +1,37 @@
(function() {
'use strict';
/**
* The Template Loader. Used to asynchronously load templates located in separate .html files
*/
app.util.tpl = {
// Hash of preloaded templates for the app
templates: {},
// Get template by name from hash of preloaded templates
get: function(name) {
return this.templates[name];
}
};
/**
* Load templates and start the application
*/
$(document).ready(function() {
console.log('sandbox loaded');
window.onmessage = function(e) {
app.util.tpl.templates = e.data;
// remember references to main window
window.mainWindow = e.source;
window.mainWindowOrigin = e.origin;
var router = new app.Router();
Backbone.history.start();
};
});
}());

View File

@ -40,15 +40,21 @@
textVisible: true,
theme: 'c'
});
this.dao.init(account, password, function(err) {
$.mobile.loading('hide');
if (err) {
window.alert(err.errMsg);
return;
}
window.location = '#accounts/' + account.get('emailAddress') + '/folders';
});
window.location = '#accounts/test@example.com/folders';
window.mainWindow.postMessage({
cmd: 'hello back from sandbox'
}, window.mainWindowOrigin);
// this.dao.init(account, password, function(err) {
// $.mobile.loading('hide');
// if (err) {
// window.alert(err.errMsg);
// return;
// }
// window.location = '#accounts/' + account.get('emailAddress') + '/folders';
// });
}
});

77
src/js/window-loader.js Normal file
View File

@ -0,0 +1,77 @@
(function() {
'use strict';
/**
* The Template Loader. Used to asynchronously load templates located in separate .html files
*/
app.util.tpl = {
// Hash of preloaded templates for the app
templates: {},
// Recursively pre-load all the templates for the app.
loadTemplates: function(names, callback) {
var that = this;
var loadTemplate = function(index) {
var name = names[index];
console.log('Loading template: ' + name);
$.get('tpl/' + name + '.html', function(data) {
that.templates[name] = data;
index++;
if (index < names.length) {
loadTemplate(index);
} else {
callback();
}
});
};
loadTemplate(0);
},
// Get template by name from hash of preloaded templates
get: function(name) {
return this.templates[name];
}
};
/**
* Load templates and start the application
*/
$(document).ready(function() {
// are we running in native app or in browser?
var isBrowser = false;
if (document.URL.indexOf("http") === 0 || document.URL.indexOf("chrome") === 0) {
isBrowser = true;
}
if (!isBrowser) {
document.addEventListener("deviceready", onDeviceReady, false);
} else {
onDeviceReady();
}
function onDeviceReady() {
console.log('Starting in Browser: ' + isBrowser);
app.util.tpl.loadTemplates([
'login',
'compose',
'folderlist',
'messagelist',
'messagelistitem',
'read'
], function() {
// set listener for events from sandbox
window.onmessage = function(e) {
console.log(e.data);
};
// init sandbox ui
var sandbox = document.getElementById('sandboxFrame').contentWindow;
sandbox.postMessage(app.util.tpl.templates, '*');
});
}
});
}());

View File

@ -0,0 +1,293 @@
/**
* Lawnchair!
* ---
* clientside json store
*
*/
var Lawnchair = function (options, callback) {
// ensure Lawnchair was called as a constructor
if (!(this instanceof Lawnchair)) return new Lawnchair(options, callback);
// lawnchair requires json
if (!JSON) throw 'JSON unavailable! Include http://www.json.org/json2.js to fix.'
// options are optional; callback is not
if (arguments.length <= 2) {
callback = (typeof arguments[0] === 'function') ? arguments[0] : arguments[1];
options = (typeof arguments[0] === 'function') ? {} : arguments[0] || {};
} else {
throw 'Incorrect # of ctor args!'
}
// default configuration
this.record = options.record || 'record' // default for records
this.name = options.name || 'records' // default name for underlying store
// mixin first valid adapter
var adapter
// if the adapter is passed in we try to load that only
if (options.adapter) {
// the argument passed should be an array of prefered adapters
// if it is not, we convert it
if(typeof(options.adapter) === 'string'){
options.adapter = [options.adapter];
}
// iterates over the array of passed adapters
for(var j = 0, k = options.adapter.length; j < k; j++){
// itirates over the array of available adapters
for (var i = Lawnchair.adapters.length-1; i >= 0; i--) {
if (Lawnchair.adapters[i].adapter === options.adapter[j]) {
adapter = Lawnchair.adapters[i].valid() ? Lawnchair.adapters[i] : undefined;
if (adapter) break
}
}
if (adapter) break
}
// otherwise find the first valid adapter for this env
}
else {
for (var i = 0, l = Lawnchair.adapters.length; i < l; i++) {
adapter = Lawnchair.adapters[i].valid() ? Lawnchair.adapters[i] : undefined
if (adapter) break
}
}
// we have failed
if (!adapter) throw 'No valid adapter.'
// yay! mixin the adapter
for (var j in adapter)
this[j] = adapter[j]
// call init for each mixed in plugin
for (var i = 0, l = Lawnchair.plugins.length; i < l; i++)
Lawnchair.plugins[i].call(this)
// init the adapter
this.init(options, callback)
}
Lawnchair.adapters = []
/**
* queues an adapter for mixin
* ===
* - ensures an adapter conforms to a specific interface
*
*/
Lawnchair.adapter = function (id, obj) {
// add the adapter id to the adapter obj
// ugly here for a cleaner dsl for implementing adapters
obj['adapter'] = id
// methods required to implement a lawnchair adapter
var implementing = 'adapter valid init keys save batch get exists all remove nuke'.split(' ')
, indexOf = this.prototype.indexOf
// mix in the adapter
for (var i in obj) {
if (indexOf(implementing, i) === -1) throw 'Invalid adapter! Nonstandard method: ' + i
}
// if we made it this far the adapter interface is valid
// insert the new adapter as the preferred adapter
Lawnchair.adapters.splice(0,0,obj)
}
Lawnchair.plugins = []
/**
* generic shallow extension for plugins
* ===
* - if an init method is found it registers it to be called when the lawnchair is inited
* - yes we could use hasOwnProp but nobody here is an asshole
*/
Lawnchair.plugin = function (obj) {
for (var i in obj)
i === 'init' ? Lawnchair.plugins.push(obj[i]) : this.prototype[i] = obj[i]
}
/**
* helpers
*
*/
Lawnchair.prototype = {
isArray: Array.isArray || function(o) { return Object.prototype.toString.call(o) === '[object Array]' },
/**
* this code exists for ie8... for more background see:
* http://www.flickr.com/photos/westcoastlogic/5955365742/in/photostream
*/
indexOf: function(ary, item, i, l) {
if (ary.indexOf) return ary.indexOf(item)
for (i = 0, l = ary.length; i < l; i++) if (ary[i] === item) return i
return -1
},
// awesome shorthand callbacks as strings. this is shameless theft from dojo.
lambda: function (callback) {
return this.fn(this.record, callback)
},
// first stab at named parameters for terse callbacks; dojo: first != best // ;D
fn: function (name, callback) {
return typeof callback == 'string' ? new Function(name, callback) : callback
},
// returns a unique identifier (by way of Backbone.localStorage.js)
// TODO investigate smaller UUIDs to cut on storage cost
uuid: function () {
var S4 = function () {
return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
}
return (S4()+S4()+"-"+S4()+"-"+S4()+"-"+S4()+"-"+S4()+S4()+S4());
},
// a classic iterator
each: function (callback) {
var cb = this.lambda(callback)
// iterate from chain
if (this.__results) {
for (var i = 0, l = this.__results.length; i < l; i++) cb.call(this, this.__results[i], i)
}
// otherwise iterate the entire collection
else {
this.all(function(r) {
for (var i = 0, l = r.length; i < l; i++) cb.call(this, r[i], i)
})
}
return this
}
// --
};
// window.name code courtesy Remy Sharp: http://24ways.org/2009/breaking-out-the-edges-of-the-browser
Lawnchair.adapter('window-name', (function() {
if (typeof window==='undefined') {
window = { top: { } }; // node/optimizer compatibility
}
// edited from the original here by elsigh
// Some sites store JSON data in window.top.name, but some folks (twitter on iPad)
// put simple strings in there - we should make sure not to cause a SyntaxError.
var data = {}
try {
data = JSON.parse(window.top.name)
} catch (e) {}
return {
valid: function () {
return typeof window.top.name != 'undefined'
},
init: function (options, callback) {
data[this.name] = data[this.name] || {index:[],store:{}}
this.index = data[this.name].index
this.store = data[this.name].store
this.fn(this.name, callback).call(this, this)
return this
},
keys: function (callback) {
this.fn('keys', callback).call(this, this.index)
return this
},
save: function (obj, cb) {
// data[key] = value + ''; // force to string
// window.top.name = JSON.stringify(data);
var key = obj.key || this.uuid()
this.exists(key, function(exists) {
if (!exists) {
if (obj.key) delete obj.key
this.index.push(key)
}
this.store[key] = obj
try {
window.top.name = JSON.stringify(data) // TODO wow, this is the only diff from the memory adapter
} catch(e) {
// restore index/store to previous value before JSON exception
if (!exists) {
this.index.pop();
delete this.store[key];
}
throw e;
}
if (cb) {
obj.key = key
this.lambda(cb).call(this, obj)
}
})
return this
},
batch: function (objs, cb) {
var r = []
for (var i = 0, l = objs.length; i < l; i++) {
this.save(objs[i], function(record) {
r.push(record)
})
}
if (cb) this.lambda(cb).call(this, r)
return this
},
get: function (keyOrArray, cb) {
var r;
if (this.isArray(keyOrArray)) {
r = []
for (var i = 0, l = keyOrArray.length; i < l; i++) {
r.push(this.store[keyOrArray[i]])
}
} else {
r = this.store[keyOrArray]
if (r) r.key = keyOrArray
}
if (cb) this.lambda(cb).call(this, r)
return this
},
exists: function (key, cb) {
this.lambda(cb).call(this, !!(this.store[key]))
return this
},
all: function (cb) {
var r = []
for (var i = 0, l = this.index.length; i < l; i++) {
var obj = this.store[this.index[i]]
obj.key = this.index[i]
r.push(obj)
}
this.fn(this.name, cb).call(this, r)
return this
},
remove: function (keyOrArray, cb) {
var del = this.isArray(keyOrArray) ? keyOrArray : [keyOrArray]
for (var i = 0, l = del.length; i < l; i++) {
var key = del[i].key ? del[i].key : del[i]
var where = this.indexOf(this.index, key)
if (where < 0) continue /* key not present */
delete this.store[key]
this.index.splice(where, 1)
}
window.top.name = JSON.stringify(data)
if (cb) this.lambda(cb).call(this)
return this
},
nuke: function (cb) {
this.store = data[this.name].store = {}
this.index = data[this.name].index = []
window.top.name = JSON.stringify(data)
if (cb) this.lambda(cb).call(this)
return this
}
}
/////
})())

View File

@ -1,14 +0,0 @@
var Lawnchair=function(e,h){if(!(this instanceof Lawnchair))return new Lawnchair(e,h);if(!JSON)throw"JSON unavailable! Include http://www.json.org/json2.js to fix.";if(arguments.length<=2){h=typeof arguments[0]==="function"?arguments[0]:arguments[1];e=typeof arguments[0]==="function"?{}:arguments[0]||{}}else throw"Incorrect # of ctor args!";this.record=e.record||"record";this.name=e.name||"records";var a;if(e.adapter){if(typeof e.adapter==="string")e.adapter=[e.adapter];for(var b=0,c=e.adapter.length;b<
c;b++){for(var d=Lawnchair.adapters.length-1;d>=0;d--)if(Lawnchair.adapters[d].adapter===e.adapter[b])if(a=Lawnchair.adapters[d].valid()?Lawnchair.adapters[d]:undefined)break;if(a)break}}else{d=0;for(c=Lawnchair.adapters.length;d<c;d++)if(a=Lawnchair.adapters[d].valid()?Lawnchair.adapters[d]:undefined)break}if(!a)throw"No valid adapter.";for(b in a)this[b]=a[b];d=0;for(c=Lawnchair.plugins.length;d<c;d++)Lawnchair.plugins[d].call(this);this.init(e,h)};Lawnchair.adapters=[];
Lawnchair.adapter=function(e,h){h.adapter=e;var a="adapter valid init keys save batch get exists all remove nuke".split(" "),b=this.prototype.indexOf,c;for(c in h)if(b(a,c)===-1)throw"Invalid adapter! Nonstandard method: "+c;Lawnchair.adapters.splice(0,0,h)};Lawnchair.plugins=[];Lawnchair.plugin=function(e){for(var h in e)h==="init"?Lawnchair.plugins.push(e[h]):this.prototype[h]=e[h]};
Lawnchair.prototype={isArray:Array.isArray||function(e){return Object.prototype.toString.call(e)==="[object Array]"},indexOf:function(e,h,a,b){if(e.indexOf)return e.indexOf(h);a=0;for(b=e.length;a<b;a++)if(e[a]===h)return a;return-1},lambda:function(e){return this.fn(this.record,e)},fn:function(e,h){return typeof h=="string"?new Function(e,h):h},uuid:function(){var e=function(){return((1+Math.random())*65536|0).toString(16).substring(1)};return e()+e()+"-"+e()+"-"+e()+"-"+e()+"-"+e()+e()+e()},each:function(e){var h=
this.lambda(e);if(this.__results){e=0;for(var a=this.__results.length;e<a;e++)h.call(this,this.__results[e],e)}else this.all(function(b){for(var c=0,d=b.length;c<d;c++)h.call(this,b[c],c)});return this}};
Lawnchair.adapter("window-name",function(){if(typeof window==="undefined")window={top:{}};var e={};try{e=JSON.parse(window.top.name)}catch(h){}return{valid:function(){return typeof window.top.name!="undefined"},init:function(a,b){e[this.name]=e[this.name]||{index:[],store:{}};this.index=e[this.name].index;this.store=e[this.name].store;this.fn(this.name,b).call(this,this);return this},keys:function(a){this.fn("keys",a).call(this,this.index);return this},save:function(a,b){var c=a.key||this.uuid();
this.exists(c,function(d){if(!d){a.key&&delete a.key;this.index.push(c)}this.store[c]=a;try{window.top.name=JSON.stringify(e)}catch(g){if(!d){this.index.pop();delete this.store[c]}throw g;}if(b){a.key=c;this.lambda(b).call(this,a)}});return this},batch:function(a,b){for(var c=[],d=0,g=a.length;d<g;d++)this.save(a[d],function(f){c.push(f)});b&&this.lambda(b).call(this,c);return this},get:function(a,b){var c;if(this.isArray(a)){c=[];for(var d=0,g=a.length;d<g;d++)c.push(this.store[a[d]])}else if(c=
this.store[a])c.key=a;b&&this.lambda(b).call(this,c);return this},exists:function(a,b){this.lambda(b).call(this,!!this.store[a]);return this},all:function(a){for(var b=[],c=0,d=this.index.length;c<d;c++){var g=this.store[this.index[c]];g.key=this.index[c];b.push(g)}this.fn(this.name,a).call(this,b);return this},remove:function(a,b){for(var c=this.isArray(a)?a:[a],d=0,g=c.length;d<g;d++){var f=c[d].key?c[d].key:c[d],i=this.indexOf(this.index,f);if(!(i<0)){delete this.store[f];this.index.splice(i,1)}}window.top.name=
JSON.stringify(e);b&&this.lambda(b).call(this);return this},nuke:function(a){this.store=e[this.name].store={};this.index=e[this.name].index=[];window.top.name=JSON.stringify(e);a&&this.lambda(a).call(this);return this}}}());
Lawnchair.adapter("dom",function(){var e=window.localStorage,h=function(a){return{key:a+"._index_",all:function(){var b=e.getItem(JSON.stringify(this.key));if(b)b=JSON.parse(b);b===null&&e.setItem(JSON.stringify(this.key),JSON.stringify([]));return JSON.parse(e.getItem(JSON.stringify(this.key)))},add:function(b){var c=this.all();c.push(b);e.setItem(JSON.stringify(this.key),JSON.stringify(c))},del:function(b){for(var c=this.all(),d=[],g=0,f=c.length;g<f;g++)c[g]!=b&&d.push(c[g]);e.setItem(JSON.stringify(this.key),
JSON.stringify(d))},find:function(b){for(var c=this.all(),d=0,g=c.length;d<g;d++)if(b===c[d])return d;return false}}};return{valid:function(){return!!e&&function(){var a=true,b=Math.random();try{e.setItem(b,b)}catch(c){a=false}e.removeItem(b);return a}()},init:function(a,b){this.indexer=h(this.name);b&&this.fn(this.name,b).call(this,this)},save:function(a,b){var c=a.key?this.name+"."+a.key:this.name+"."+this.uuid();delete a.key;e.setItem(c,JSON.stringify(a));this.indexer.find(c)===false&&this.indexer.add(c);
a.key=c.slice(this.name.length+1);b&&this.lambda(b).call(this,a);return this},batch:function(a,b){for(var c=[],d=0,g=a.length;d<g;d++)this.save(a[d],function(f){c.push(f)});b&&this.lambda(b).call(this,c);return this},keys:function(a){if(a){var b=this.name,c=this.indexer.all(),d=[];if(Array.prototype.map)d=c.map(function(f){return f.replace(b+".","")});else for(var g in c)d.push(g.replace(b+".",""));this.fn("keys",a).call(this,d)}return this},get:function(a,b){if(this.isArray(a)){for(var c=[],d=0,
g=a.length;d<g;d++){var f=this.name+"."+a[d];if(f=e.getItem(f)){f=JSON.parse(f);f.key=a[d]}c.push(f)}b&&this.lambda(b).call(this,c)}else{f=this.name+"."+a;if(f=e.getItem(f)){f=JSON.parse(f);f.key=a}b&&this.lambda(b).call(this,f)}return this},exists:function(a,b){var c=this.indexer.find(this.name+"."+a)===false?false:true;this.lambda(b).call(this,c);return this},all:function(a){for(var b=this.indexer.all(),c=[],d,g,f=0,i=b.length;f<i;f++){g=b[f];d=JSON.parse(e.getItem(g));d.key=g.replace(this.name+
".","");c.push(d)}a&&this.fn(this.name,a).call(this,c);return this},remove:function(a,b){var c=this;if(this.isArray(a)){var d,g=a.length,f=function(i){c.remove(a[i],function(){--g>0||b&&c.lambda(b).call(c)})};for(d=0;d<a.length;d++)f(d);return this}d=this.name+"."+(a.key?a.key:a);this.indexer.del(d);e.removeItem(d);b&&this.lambda(b).call(this);return this},nuke:function(a){this.all(function(b){for(var c=0,d=b.length;c<d;c++)this.remove(b[c]);a&&this.lambda(a).call(this)});return this}}}());

19
src/manifest.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "Whiteout Mail",
"description": "An offline enable mail app with integrated end-2-end messaging and a client-side encrypted message store both locally and in the cloud",
"version": "0.0.1",
"manifest_version": 2,
"offline_enabled": true,
"permissions": [
"http://storage.whiteout.io/"
],
"app": {
"background": {
"scripts": ["background.js"]
}
},
"sandbox": {
"pages": ["sandbox.html"],
"content_security_policy": "sandbox allow-scripts; default-src 'self'; object-src 'none'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'"
}
}

40
src/sandbox.html Normal file
View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Mail</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/jquery.mobile-1.2.0.min.css"/>
<link rel="stylesheet" href="css/styles.css"/>
<!-- The Scripts -->
<script src="lib/jquery-1.8.2.min.js"></script>
<script src="lib/underscore-1.4.4.min.js"></script>
<script src="lib/backbone-1.0.0.min.js"></script>
<script src="js/jqm-config.js"></script>
<script src="lib/jquery.mobile-1.2.0.min.js"></script>
<script src="lib/forge/forge.rsa.bundle.js"></script>
<script src="lib/uuid.js"></script>
<script src="js/app-config.js"></script>
<script src="js/model/email-model.js"></script>
<script src="js/model/folder-model.js"></script>
<script src="js/model/account-model.js"></script>
<script src="js/model/publickey-model.js"></script>
<script src="js/view/login-view.js"></script>
<script src="js/view/compose-view.js"></script>
<script src="js/view/messagelist-view.js"></script>
<script src="js/view/messagelistitem-view.js"></script>
<script src="js/view/folderlist-view.js"></script>
<script src="js/view/read-view.js"></script>
<script src="js/app-router.js"></script>
<script src="js/sandbox-loader.js"></script>
</head>
<body></body>
</html>

View File

@ -20,7 +20,7 @@
<script src="../../src/lib/jquery-1.8.2.min.js"></script>
<script src="../../src/lib/underscore-1.4.4.min.js"></script>
<script src="../../src/lib/backbone-1.0.0.min.js"></script>
<script src="../../src/lib/lawnchair/lawnchair-git.min.js"></script>
<script src="../../src/lib/lawnchair/lawnchair-git.js"></script>
<script src="../../src/lib/lawnchair/lawnchair-adapter-webkit-sqlite-git.js"></script>
<script src="../../src/lib/lawnchair/lawnchair-adapter-indexed-db-git.js"></script>

View File

@ -20,7 +20,7 @@
<script src="../../src/lib/jquery-1.8.2.min.js"></script>
<script src="../../src/lib/underscore-1.4.4.min.js"></script>
<script src="../../src/lib/backbone-1.0.0.min.js"></script>
<script src="../../src/lib/lawnchair/lawnchair-git.min.js"></script>
<script src="../../src/lib/lawnchair/lawnchair-git.js"></script>
<script src="../../src/lib/lawnchair/lawnchair-adapter-webkit-sqlite-git.js"></script>
<script src="../../src/lib/lawnchair/lawnchair-adapter-indexed-db-git.js"></script>