Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
40
gaia/distros/spark/apps/studio/js/activity_handler.js
Executable file
40
gaia/distros/spark/apps/studio/js/activity_handler.js
Executable file
|
|
@ -0,0 +1,40 @@
|
|||
/*global
|
||||
AutoTheme,
|
||||
Main
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
if (!window.navigator.mozSetMessageHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ActivityDataType = {
|
||||
IMAGE: 'image/*'
|
||||
};
|
||||
|
||||
var ActivityHandler = {
|
||||
onShareActivity(activity) {
|
||||
var activityData = activity.source.data;
|
||||
|
||||
if (activityData.type !== ActivityDataType.IMAGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!activityData.blobs.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var image = activityData.blobs[0];
|
||||
Navigation.popToRoot().then(() => {
|
||||
Main.createTheme();
|
||||
AutoTheme.load(image);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.navigator.mozSetMessageHandler(
|
||||
'activity', ActivityHandler.onShareActivity.bind(ActivityHandler)
|
||||
);
|
||||
|
||||
})(window);
|
||||
192
gaia/distros/spark/apps/studio/js/autotheme.js
Executable file
192
gaia/distros/spark/apps/studio/js/autotheme.js
Executable file
|
|
@ -0,0 +1,192 @@
|
|||
/*global
|
||||
Color,
|
||||
ColorThief,
|
||||
Defer,
|
||||
MozActivity
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @exports AutoTheme
|
||||
* @event AutoTheme:palette This is emitted when a new palete has been generated
|
||||
* from an image.
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
// is autotheming active
|
||||
var active;
|
||||
|
||||
function loadImage(src) {
|
||||
var defer = new Defer();
|
||||
|
||||
var img = new Image();
|
||||
img.src = src;
|
||||
|
||||
if (img.complete) {
|
||||
defer.resolve(img);
|
||||
} else {
|
||||
img.onload = () => defer.resolve(img);
|
||||
img.onerror = defer.reject;
|
||||
}
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
function blobFromURL(src) {
|
||||
var defer = new Defer();
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.addEventListener('load', () => {
|
||||
defer.resolve(xhr.response);
|
||||
});
|
||||
xhr.addEventListener('error', defer.reject);
|
||||
|
||||
xhr.open('GET', src);
|
||||
xhr.responseType = 'blob';
|
||||
xhr.send();
|
||||
|
||||
return defer.promise;
|
||||
}
|
||||
|
||||
var AutoTheme = exports.AutoTheme = {
|
||||
get active() {
|
||||
return active;
|
||||
},
|
||||
set active(bool) {
|
||||
active = bool;
|
||||
document.body.classList.toggle('has-autotheme', bool);
|
||||
},
|
||||
elts: {
|
||||
commandCreate: document.querySelectorAll('.autotheme-command-create'),
|
||||
commandCancel: document.querySelectorAll('.autotheme-command-cancel')
|
||||
},
|
||||
|
||||
handleEvent(e) {
|
||||
switch(e.type) {
|
||||
case 'click':
|
||||
this.pickImage()
|
||||
.then(this.getPalette.bind(this))
|
||||
.then(this.storePalette.bind(this));
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/* called from activity */
|
||||
load(blob) {
|
||||
return this.getPalette(blob)
|
||||
.then(this.storePalette.bind(this));
|
||||
},
|
||||
|
||||
pickImage() {
|
||||
var defer = new Defer();
|
||||
|
||||
if (window.MozActivity) {
|
||||
var activity = new MozActivity({
|
||||
name: 'pick',
|
||||
data: { type: 'image/*' }
|
||||
});
|
||||
|
||||
activity.onsuccess = () => defer.resolve(activity.result.blob);
|
||||
activity.onerror = defer.reject;
|
||||
} else {
|
||||
defer.resolve(blobFromURL('img/default_image.jpg'));
|
||||
}
|
||||
|
||||
return defer.promise;
|
||||
},
|
||||
|
||||
getPalette(blob) {
|
||||
function imageFromBlob(blob) {
|
||||
var blobUrl = window.URL.createObjectURL(blob);
|
||||
return loadImage(blobUrl).then((image) => {
|
||||
window.URL.revokeObjectURL(image.src);
|
||||
return image;
|
||||
});
|
||||
}
|
||||
|
||||
this.image = blob;
|
||||
|
||||
var defer = new Defer();
|
||||
|
||||
imageFromBlob(blob).then((image) => {
|
||||
var colorThief = new ColorThief();
|
||||
var palette = colorThief.getPalette(image, 10);
|
||||
defer.resolve(palette);
|
||||
});
|
||||
|
||||
return defer.promise;
|
||||
},
|
||||
|
||||
storePalette(palette) {
|
||||
if (palette === null) {
|
||||
this.clean();
|
||||
} else {
|
||||
this.palette = palette.map(Color);
|
||||
this.active = true;
|
||||
}
|
||||
this.emit('palette');
|
||||
},
|
||||
|
||||
showPalette(where) {
|
||||
if (!where) {
|
||||
return;
|
||||
}
|
||||
|
||||
where.textContent = '';
|
||||
|
||||
if (!this.palette) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.palette.forEach((color) => {
|
||||
var elt = document.createElement('div');
|
||||
elt.className = 'palette-item';
|
||||
elt.style.backgroundColor = color.toCSS();
|
||||
elt.dataset.color = color.toJSONString();
|
||||
where.appendChild(elt);
|
||||
});
|
||||
},
|
||||
|
||||
clean() {
|
||||
this.active = false;
|
||||
this.palette = null;
|
||||
this.image = null;
|
||||
},
|
||||
|
||||
asStorable() {
|
||||
if (!this.active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
palette: this.palette.map((color) => color.toStorable()),
|
||||
image: this.image
|
||||
};
|
||||
},
|
||||
|
||||
fromStorable(stored) {
|
||||
if (stored === null) {
|
||||
this.clean();
|
||||
return;
|
||||
}
|
||||
|
||||
this.palette = stored.palette.map(Color.fromStorable);
|
||||
this.image = stored.image;
|
||||
this.active = true;
|
||||
this.emit('palette');
|
||||
},
|
||||
|
||||
emit(name) {
|
||||
var event = new CustomEvent('AutoTheme:' + name);
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
};
|
||||
|
||||
Array.from(AutoTheme.elts.commandCreate).forEach(
|
||||
elt => elt.addEventListener('click', AutoTheme)
|
||||
);
|
||||
Array.from(AutoTheme.elts.commandCancel).forEach(
|
||||
elt => elt.addEventListener('click', () => AutoTheme.storePalette(null))
|
||||
);
|
||||
})(window);
|
||||
55
gaia/distros/spark/apps/studio/js/color.js
Executable file
55
gaia/distros/spark/apps/studio/js/color.js
Executable file
|
|
@ -0,0 +1,55 @@
|
|||
/*global GaiaColorPicker */
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
function Color(color) {
|
||||
if (! (this instanceof Color)) {
|
||||
return new Color(color);
|
||||
}
|
||||
|
||||
if (Array.isArray(color)) {
|
||||
color = {
|
||||
r: color[0],
|
||||
g: color[1],
|
||||
b: color[2]
|
||||
};
|
||||
}
|
||||
|
||||
this.r = +color.r;
|
||||
this.g = +color.g;
|
||||
this.b = +color.b;
|
||||
}
|
||||
|
||||
Color.prototype = {
|
||||
toCSS() {
|
||||
return 'rgb(' + this.r + ',' + this.g + ',' + this.b + ')';
|
||||
},
|
||||
|
||||
toStorable() {
|
||||
return {
|
||||
r: this.r,
|
||||
g: this.g,
|
||||
b: this.b
|
||||
};
|
||||
},
|
||||
|
||||
toJSONString() {
|
||||
return JSON.stringify(this);
|
||||
},
|
||||
|
||||
toHexString() {
|
||||
// Ideally GaiaColorPicker should use the implementation here
|
||||
return GaiaColorPicker.rgb2hex(this.r, this.g, this.b);
|
||||
}
|
||||
};
|
||||
|
||||
Color.fromJSONString = function(json) {
|
||||
return Color(JSON.parse(json));
|
||||
};
|
||||
|
||||
Color.fromStorable = function(obj) {
|
||||
return Color(obj);
|
||||
};
|
||||
|
||||
exports.Color = Color;
|
||||
})(window);
|
||||
12
gaia/distros/spark/apps/studio/js/defer.js
Executable file
12
gaia/distros/spark/apps/studio/js/defer.js
Executable file
|
|
@ -0,0 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
exports.Defer = function() {
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
});
|
||||
};
|
||||
})(window);
|
||||
|
||||
|
||||
172
gaia/distros/spark/apps/studio/js/details.js
Executable file
172
gaia/distros/spark/apps/studio/js/details.js
Executable file
|
|
@ -0,0 +1,172 @@
|
|||
/* global
|
||||
AutoTheme,
|
||||
Edit,
|
||||
Generation,
|
||||
Main,
|
||||
Navigation,
|
||||
Storage
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var currentTheme = null;
|
||||
|
||||
var Details = {
|
||||
panel: document.getElementById('details'),
|
||||
header: document.querySelector('#details gaia-header'),
|
||||
title: document.querySelector('#details gaia-header h1'),
|
||||
list: document.querySelector('#details gaia-list'),
|
||||
autotheme: document.querySelector('#details .autotheme-palette'),
|
||||
enableButton: document.getElementById('enable-theme-button'),
|
||||
|
||||
prepareForDisplay: function(params) {
|
||||
Array.from(this.list.children).forEach((item) => {
|
||||
if (!item.classList.contains('static')) {
|
||||
item.remove();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('AutoTheme:palette', this.onPalette);
|
||||
|
||||
return Storage.fetchTheme(params.id).then((theme) => {
|
||||
currentTheme = theme;
|
||||
this.title.textContent = theme.title;
|
||||
this.header.setAttr('action', 'back');
|
||||
|
||||
if (theme.autotheme) {
|
||||
AutoTheme.fromStorable(theme.autotheme);
|
||||
}
|
||||
|
||||
Object.keys(theme.groups).forEach(function(group) {
|
||||
var sectionTitle = document.createElement('span');
|
||||
sectionTitle.classList.add('group');
|
||||
sectionTitle.textContent = group;
|
||||
this.list.appendChild(sectionTitle);
|
||||
|
||||
Object.keys(theme.groups[group]).forEach(function(key, index) {
|
||||
var link = document.createElement('a');
|
||||
link.classList.add('navigation');
|
||||
link.dataset.group = group;
|
||||
link.dataset.section = key;
|
||||
if (index === 0) {
|
||||
link.classList.add('first');
|
||||
}
|
||||
|
||||
var title = document.createElement('h3');
|
||||
title.textContent = key;
|
||||
link.appendChild(title);
|
||||
|
||||
var forward = document.createElement('i');
|
||||
forward.dataset.icon = 'forward-light';
|
||||
link.appendChild(forward);
|
||||
|
||||
this.list.appendChild(link);
|
||||
}, this);
|
||||
}, this);
|
||||
|
||||
if (currentTheme.manifestURL) {
|
||||
this.enableButton.disabled = true;
|
||||
} else {
|
||||
this.enableButton.disabled = false;
|
||||
}
|
||||
}).catch(function(error) {
|
||||
console.log(error);
|
||||
}).then(() => this.panel);
|
||||
},
|
||||
|
||||
onPalette() {
|
||||
/* no "this" available here. */
|
||||
AutoTheme.showPalette(Details.autotheme);
|
||||
currentTheme.autotheme = AutoTheme.asStorable();
|
||||
Storage.updateTheme(currentTheme);
|
||||
},
|
||||
|
||||
refreshTheme: function() {
|
||||
return Storage.fetchTheme(currentTheme.id)
|
||||
.then((theme) => { currentTheme = theme; });
|
||||
},
|
||||
|
||||
installTheme: function() {
|
||||
return Generation.installTheme(currentTheme.id, true)
|
||||
.then(this.refreshTheme);
|
||||
},
|
||||
|
||||
forkTheme: function() {
|
||||
return Main.promptDuplicate().then(function(title) {
|
||||
return Storage.forkTheme(currentTheme, title);
|
||||
});
|
||||
},
|
||||
|
||||
removeTheme: function() {
|
||||
return Main.promptDeleteTheme().then(function() {
|
||||
return Generation.uninstallIfNeeded(currentTheme)
|
||||
.then(Storage.removeTheme.bind(Storage, currentTheme.id));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Details.panel.addEventListener('click', function(evt) {
|
||||
var target = evt.target;
|
||||
|
||||
if (target.dataset.action == 'install') {
|
||||
target.disabled = true;
|
||||
Details.installTheme().then(() => {
|
||||
|
||||
}).catch(console.error.bind(console));
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.dataset.action == 'fork') {
|
||||
Details.forkTheme().then(function() {
|
||||
Main.prepareForDisplay();
|
||||
Navigation.pop();
|
||||
}).catch(function(error) {
|
||||
console.log(error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (target.dataset.action == 'remove') {
|
||||
Details.removeTheme().then(function() {
|
||||
Main.prepareForDisplay();
|
||||
Navigation.pop();
|
||||
}).catch(function(error) {
|
||||
console.log(error);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.classList.contains('navigation')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var targetGroup = target.dataset.group;
|
||||
var targetSection = target.dataset.section;
|
||||
Navigation.push(Edit.prepareForDisplay({
|
||||
theme: currentTheme,
|
||||
group: targetGroup,
|
||||
section: targetSection
|
||||
}));
|
||||
});
|
||||
|
||||
Details.header.addEventListener('action', function(evt) {
|
||||
if (evt.detail.type != 'back') {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigation.pop();
|
||||
});
|
||||
|
||||
Details.panel.addEventListener('Navigation:pop', function onPop() {
|
||||
window.removeEventListener('AutoTheme:palette', Details.onPalette);
|
||||
AutoTheme.clean();
|
||||
AutoTheme.showPalette(Details.autotheme);
|
||||
});
|
||||
|
||||
Details.panel.addEventListener('Navigation:display', function onPop() {
|
||||
Details.refreshTheme();
|
||||
});
|
||||
|
||||
exports.Details = Details;
|
||||
})(window);
|
||||
193
gaia/distros/spark/apps/studio/js/edit.js
Executable file
193
gaia/distros/spark/apps/studio/js/edit.js
Executable file
|
|
@ -0,0 +1,193 @@
|
|||
/*global AutoTheme, Color, Navigation, Storage */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var currentTheme = null;
|
||||
var currentSection = null;
|
||||
var currentKey = null;
|
||||
|
||||
var groupName = null;
|
||||
var sectionName = null;
|
||||
|
||||
var Edit = {
|
||||
panel: document.getElementById('edit'),
|
||||
header: document.querySelector('#edit gaia-header'),
|
||||
title: document.querySelector('#edit gaia-header h1'),
|
||||
list: document.querySelector('#edit-list'),
|
||||
editColor: document.querySelector('#edit-color'),
|
||||
picker: document.querySelector('#edit-color gaia-color-picker'),
|
||||
iframe: document.querySelector('#edit iframe'),
|
||||
autotheme: document.querySelector('#edit-color .autotheme-palette'),
|
||||
|
||||
prepareForDisplay: function(params) {
|
||||
var defer = new Defer();
|
||||
|
||||
currentTheme = params.theme;
|
||||
|
||||
this.title.textContent = params.group + ' / ' + params.section;
|
||||
this.header.setAttr('action', 'back');
|
||||
AutoTheme.showPalette(this.autotheme);
|
||||
|
||||
this.picker.addEventListener('change', this.onPickerChange.bind(this));
|
||||
|
||||
var currentList = this.list.querySelector('gaia-list');
|
||||
if (currentList) {
|
||||
this.list.removeChild(currentList);
|
||||
}
|
||||
|
||||
var list = document.createElement('gaia-list');
|
||||
currentSection = currentTheme.groups[params.group][params.section];
|
||||
|
||||
groupName = params.group;
|
||||
sectionName = params.section;
|
||||
|
||||
this.iframe.src = params.section + '-preview.html';
|
||||
this.iframe.onload = () => {
|
||||
Object.keys(currentSection).forEach((key) => {
|
||||
var body = this.iframe.contentDocument.body;
|
||||
body.style.setProperty(key, currentSection[key]);
|
||||
});
|
||||
|
||||
defer.resolve();
|
||||
};
|
||||
|
||||
Object.keys(currentSection).forEach((key) => {
|
||||
var link = document.createElement('a');
|
||||
link.classList.add('edit');
|
||||
link.dataset.key = key;
|
||||
|
||||
var label = document.createElement('label');
|
||||
label.classList.add('l-flex');
|
||||
label.classList.add('l-flex-grow');
|
||||
|
||||
var title = document.createElement('h3');
|
||||
title.textContent = key;
|
||||
label.appendChild(title);
|
||||
|
||||
link.appendChild(label);
|
||||
|
||||
var valueElt = document.createElement('div');
|
||||
var value = currentSection[key];
|
||||
valueElt.setAttribute('aria-label', value);
|
||||
valueElt.dataset.id = key;
|
||||
valueElt.className = 'palette-item';
|
||||
valueElt.style.backgroundColor = value;
|
||||
|
||||
link.appendChild(valueElt);
|
||||
|
||||
list.appendChild(link);
|
||||
});
|
||||
|
||||
this.list.appendChild(list);
|
||||
this.list.scrollTop = 0;
|
||||
|
||||
return defer.promise.then(() => this.panel);
|
||||
},
|
||||
|
||||
pick: function(key) {
|
||||
currentKey = key;
|
||||
this.picker.value = currentSection[key];
|
||||
this.editColor.classList.add('editing');
|
||||
this.picker.resize();
|
||||
},
|
||||
|
||||
onPickerChange() {
|
||||
if (!currentKey) {
|
||||
return;
|
||||
}
|
||||
var value = this.picker.value;
|
||||
this.iframe.contentDocument.body.style.setProperty(currentKey, value);
|
||||
}
|
||||
};
|
||||
|
||||
Edit.header.addEventListener('action', function(evt) {
|
||||
if (evt.detail.type != 'back') {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigation.pop();
|
||||
});
|
||||
|
||||
Edit.panel.addEventListener('click', function(evt) {
|
||||
var target = evt.target;
|
||||
if (!target.classList.contains('edit')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Edit.pick(target.dataset.key);
|
||||
});
|
||||
|
||||
Edit.panel.addEventListener('Navigation:pop', function onPop() {
|
||||
Edit.editColor.classList.remove('editing');
|
||||
Edit.iframe.src = '';
|
||||
});
|
||||
|
||||
Edit.editColor.addEventListener('click', function(evt) {
|
||||
var target = evt.target;
|
||||
var value;
|
||||
|
||||
if (target.classList.contains('save')) {
|
||||
if (!currentKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = Edit.picker.value;
|
||||
currentSection[currentKey] = value;
|
||||
|
||||
var elem = Edit.list.querySelector('[data-id=' + currentKey + ']');
|
||||
elem.setAttribute('aria-label', value);
|
||||
elem.style.backgroundColor = value;
|
||||
|
||||
Storage.updateTheme(currentTheme)
|
||||
.then(() => {
|
||||
Edit.editColor.classList.remove('editing');
|
||||
currentKey = null;
|
||||
})
|
||||
.then(() => {
|
||||
if (!currentTheme.manifestURL) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Updating the installed theme
|
||||
return Generation.installTheme(currentTheme.id, false)
|
||||
.then(Storage.fetchTheme.bind(null, currentTheme.id))
|
||||
.then((theme) => {
|
||||
// Refresh
|
||||
currentTheme = theme;
|
||||
currentSection =
|
||||
currentTheme.groups[groupName][sectionName];
|
||||
});
|
||||
})
|
||||
.catch(function(error) {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.classList.contains('cancel')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentKey) {
|
||||
value = currentSection[currentKey];
|
||||
Edit.iframe.contentDocument.body.style.setProperty(currentKey, value);
|
||||
}
|
||||
|
||||
Edit.editColor.classList.remove('editing');
|
||||
currentKey = null;
|
||||
});
|
||||
|
||||
Edit.autotheme.addEventListener('click', function(e) {
|
||||
if (!e.target.classList.contains('palette-item')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var color = Color.fromJSONString(e.target.dataset.color);
|
||||
Edit.picker.value = color.toHexString();
|
||||
Edit.onPickerChange();
|
||||
});
|
||||
|
||||
exports.Edit = Edit;
|
||||
})(window);
|
||||
609
gaia/distros/spark/apps/studio/js/ext/color-thief.js
Executable file
609
gaia/distros/spark/apps/studio/js/ext/color-thief.js
Executable file
|
|
@ -0,0 +1,609 @@
|
|||
/*!
|
||||
* Color Thief v2.0
|
||||
* by Lokesh Dhakar - http://www.lokeshdhakar.com
|
||||
*
|
||||
* License
|
||||
* -------
|
||||
* Creative Commons Attribution 2.5 License:
|
||||
* http://creativecommons.org/licenses/by/2.5/
|
||||
*
|
||||
* Thanks
|
||||
* ------
|
||||
* Nick Rabinowitz - For creating quantize.js.
|
||||
* John Schulz - For clean up and optimization. @JFSIII
|
||||
* Nathan Spady - For adding drag and drop support to the demo page.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
/*
|
||||
CanvasImage Class
|
||||
Class that wraps the html image element and canvas.
|
||||
It also simplifies some of the canvas context manipulation
|
||||
with a set of helper functions.
|
||||
*/
|
||||
var CanvasImage = function (image) {
|
||||
this.canvas = document.createElement('canvas');
|
||||
this.context = this.canvas.getContext('2d');
|
||||
|
||||
document.body.appendChild(this.canvas);
|
||||
|
||||
this.width = this.canvas.width = image.width;
|
||||
this.height = this.canvas.height = image.height;
|
||||
|
||||
this.context.drawImage(image, 0, 0, this.width, this.height);
|
||||
};
|
||||
|
||||
CanvasImage.prototype.clear = function () {
|
||||
this.context.clearRect(0, 0, this.width, this.height);
|
||||
};
|
||||
|
||||
CanvasImage.prototype.update = function (imageData) {
|
||||
this.context.putImageData(imageData, 0, 0);
|
||||
};
|
||||
|
||||
CanvasImage.prototype.getPixelCount = function () {
|
||||
return this.width * this.height;
|
||||
};
|
||||
|
||||
CanvasImage.prototype.getImageData = function () {
|
||||
return this.context.getImageData(0, 0, this.width, this.height);
|
||||
};
|
||||
|
||||
CanvasImage.prototype.removeCanvas = function () {
|
||||
this.canvas.parentNode.removeChild(this.canvas);
|
||||
};
|
||||
|
||||
|
||||
var ColorThief = function () {};
|
||||
|
||||
/*
|
||||
* getColor(sourceImage[, quality])
|
||||
* returns {r: num, g: num, b: num}
|
||||
*
|
||||
* Use the median cut algorithm provided by quantize.js to cluster similar
|
||||
* colors and return the base color from the largest cluster.
|
||||
*
|
||||
* Quality is an optional argument. It needs to be an integer. 0 is the highest quality settings.
|
||||
* 10 is the default. There is a trade-off between quality and speed. The bigger the number, the
|
||||
* faster a color will be returned but the greater the likelihood that it will not be the visually
|
||||
* most dominant color.
|
||||
*
|
||||
* */
|
||||
ColorThief.prototype.getColor = function(sourceImage, quality) {
|
||||
var palette = this.getPalette(sourceImage, 5, quality);
|
||||
var dominantColor = palette[0];
|
||||
return dominantColor;
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* getPalette(sourceImage[, colorCount, quality])
|
||||
* returns array[ {r: num, g: num, b: num}, {r: num, g: num, b: num}, ...]
|
||||
*
|
||||
* Use the median cut algorithm provided by quantize.js to cluster similar colors.
|
||||
*
|
||||
* colorCount determines the size of the palette; the number of colors returned. If not set, it
|
||||
* defaults to 10.
|
||||
*
|
||||
* BUGGY: Function does not always return the requested amount of colors. It can be +/- 2.
|
||||
*
|
||||
* quality is an optional argument. It needs to be an integer. 0 is the highest quality settings.
|
||||
* 10 is the default. There is a trade-off between quality and speed. The bigger the number, the
|
||||
* faster the palette generation but the greater the likelihood that colors will be missed.
|
||||
*
|
||||
*
|
||||
*/
|
||||
ColorThief.prototype.getPalette = function(sourceImage, colorCount, quality) {
|
||||
|
||||
if (typeof colorCount === 'undefined') {
|
||||
colorCount = 10;
|
||||
}
|
||||
if (typeof quality === 'undefined') {
|
||||
quality = 10;
|
||||
}
|
||||
|
||||
// Create custom CanvasImage object
|
||||
var image = new CanvasImage(sourceImage);
|
||||
var imageData = image.getImageData();
|
||||
var pixels = imageData.data;
|
||||
var pixelCount = image.getPixelCount();
|
||||
|
||||
// Store the RGB values in an array format suitable for quantize function
|
||||
var pixelArray = [];
|
||||
for (var i = 0, offset, r, g, b, a; i < pixelCount; i = i + quality) {
|
||||
offset = i * 4;
|
||||
r = pixels[offset + 0];
|
||||
g = pixels[offset + 1];
|
||||
b = pixels[offset + 2];
|
||||
a = pixels[offset + 3];
|
||||
// If pixel is mostly opaque and not white
|
||||
if (a >= 125) {
|
||||
if (!(r > 250 && g > 250 && b > 250)) {
|
||||
pixelArray.push([r, g, b]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send array to quantize function which clusters values
|
||||
// using median cut algorithm
|
||||
var cmap = MMCQ.quantize(pixelArray, colorCount);
|
||||
var palette = cmap.palette();
|
||||
|
||||
// Clean up
|
||||
image.removeCanvas();
|
||||
|
||||
return palette;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/*!
|
||||
* quantize.js Copyright 2008 Nick Rabinowitz.
|
||||
* Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
|
||||
*/
|
||||
|
||||
// fill out a couple protovis dependencies
|
||||
/*!
|
||||
* Block below copied from Protovis: http://mbostock.github.com/protovis/
|
||||
* Copyright 2010 Stanford Visualization Group
|
||||
* Licensed under the BSD License: http://www.opensource.org/licenses/bsd-license.php
|
||||
*/
|
||||
if (!pv) {
|
||||
var pv = {
|
||||
map: function(array, f) {
|
||||
var o = {};
|
||||
return f ? array.map(function(d, i) { o.index = i; return f.call(o, d); }) : array.slice();
|
||||
},
|
||||
naturalOrder: function(a, b) {
|
||||
return (a < b) ? -1 : ((a > b) ? 1 : 0);
|
||||
},
|
||||
sum: function(array, f) {
|
||||
var o = {};
|
||||
return array.reduce(f ? function(p, d, i) { o.index = i; return p + f.call(o, d); } : function(p, d) { return p + d; }, 0);
|
||||
},
|
||||
max: function(array, f) {
|
||||
return Math.max.apply(null, f ? pv.map(array, f) : array);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Basic Javascript port of the MMCQ (modified median cut quantization)
|
||||
* algorithm from the Leptonica library (http://www.leptonica.com/).
|
||||
* Returns a color map you can use to map original pixels to the reduced
|
||||
* palette. Still a work in progress.
|
||||
*
|
||||
* @author Nick Rabinowitz
|
||||
* @example
|
||||
|
||||
// array of pixels as [R,G,B] arrays
|
||||
var myPixels = [[190,197,190], [202,204,200], [207,214,210], [211,214,211], [205,207,207]
|
||||
// etc
|
||||
];
|
||||
var maxColors = 4;
|
||||
|
||||
var cmap = MMCQ.quantize(myPixels, maxColors);
|
||||
var newPalette = cmap.palette();
|
||||
var newPixels = myPixels.map(function(p) {
|
||||
return cmap.map(p);
|
||||
});
|
||||
|
||||
*/
|
||||
var MMCQ = (function() {
|
||||
// private constants
|
||||
var sigbits = 5,
|
||||
rshift = 8 - sigbits,
|
||||
maxIterations = 1000,
|
||||
fractByPopulations = 0.75;
|
||||
|
||||
// get reduced-space color index for a pixel
|
||||
function getColorIndex(r, g, b) {
|
||||
return (r << (2 * sigbits)) + (g << sigbits) + b;
|
||||
}
|
||||
|
||||
// Simple priority queue
|
||||
function PQueue(comparator) {
|
||||
var contents = [],
|
||||
sorted = false;
|
||||
|
||||
function sort() {
|
||||
contents.sort(comparator);
|
||||
sorted = true;
|
||||
}
|
||||
|
||||
return {
|
||||
push: function(o) {
|
||||
contents.push(o);
|
||||
sorted = false;
|
||||
},
|
||||
peek: function(index) {
|
||||
if (!sorted) sort();
|
||||
if (index===undefined) index = contents.length - 1;
|
||||
return contents[index];
|
||||
},
|
||||
pop: function() {
|
||||
if (!sorted) sort();
|
||||
return contents.pop();
|
||||
},
|
||||
size: function() {
|
||||
return contents.length;
|
||||
},
|
||||
map: function(f) {
|
||||
return contents.map(f);
|
||||
},
|
||||
debug: function() {
|
||||
if (!sorted) sort();
|
||||
return contents;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 3d color space box
|
||||
function VBox(r1, r2, g1, g2, b1, b2, histo) {
|
||||
var vbox = this;
|
||||
vbox.r1 = r1;
|
||||
vbox.r2 = r2;
|
||||
vbox.g1 = g1;
|
||||
vbox.g2 = g2;
|
||||
vbox.b1 = b1;
|
||||
vbox.b2 = b2;
|
||||
vbox.histo = histo;
|
||||
}
|
||||
VBox.prototype = {
|
||||
volume: function(force) {
|
||||
var vbox = this;
|
||||
if (!vbox._volume || force) {
|
||||
vbox._volume = ((vbox.r2 - vbox.r1 + 1) * (vbox.g2 - vbox.g1 + 1) * (vbox.b2 - vbox.b1 + 1));
|
||||
}
|
||||
return vbox._volume;
|
||||
},
|
||||
count: function(force) {
|
||||
var vbox = this,
|
||||
histo = vbox.histo;
|
||||
if (!vbox._count_set || force) {
|
||||
var npix = 0,
|
||||
i, j, k;
|
||||
for (i = vbox.r1; i <= vbox.r2; i++) {
|
||||
for (j = vbox.g1; j <= vbox.g2; j++) {
|
||||
for (k = vbox.b1; k <= vbox.b2; k++) {
|
||||
index = getColorIndex(i,j,k);
|
||||
npix += (histo[index] || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
vbox._count = npix;
|
||||
vbox._count_set = true;
|
||||
}
|
||||
return vbox._count;
|
||||
},
|
||||
copy: function() {
|
||||
var vbox = this;
|
||||
return new VBox(vbox.r1, vbox.r2, vbox.g1, vbox.g2, vbox.b1, vbox.b2, vbox.histo);
|
||||
},
|
||||
avg: function(force) {
|
||||
var vbox = this,
|
||||
histo = vbox.histo;
|
||||
if (!vbox._avg || force) {
|
||||
var ntot = 0,
|
||||
mult = 1 << (8 - sigbits),
|
||||
rsum = 0,
|
||||
gsum = 0,
|
||||
bsum = 0,
|
||||
hval,
|
||||
i, j, k, histoindex;
|
||||
for (i = vbox.r1; i <= vbox.r2; i++) {
|
||||
for (j = vbox.g1; j <= vbox.g2; j++) {
|
||||
for (k = vbox.b1; k <= vbox.b2; k++) {
|
||||
histoindex = getColorIndex(i,j,k);
|
||||
hval = histo[histoindex] || 0;
|
||||
ntot += hval;
|
||||
rsum += (hval * (i + 0.5) * mult);
|
||||
gsum += (hval * (j + 0.5) * mult);
|
||||
bsum += (hval * (k + 0.5) * mult);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ntot) {
|
||||
vbox._avg = [~~(rsum/ntot), ~~(gsum/ntot), ~~(bsum/ntot)];
|
||||
} else {
|
||||
// console.log('empty box');
|
||||
vbox._avg = [
|
||||
~~(mult * (vbox.r1 + vbox.r2 + 1) / 2),
|
||||
~~(mult * (vbox.g1 + vbox.g2 + 1) / 2),
|
||||
~~(mult * (vbox.b1 + vbox.b2 + 1) / 2)
|
||||
];
|
||||
}
|
||||
}
|
||||
return vbox._avg;
|
||||
},
|
||||
contains: function(pixel) {
|
||||
var vbox = this,
|
||||
rval = pixel[0] >> rshift;
|
||||
gval = pixel[1] >> rshift;
|
||||
bval = pixel[2] >> rshift;
|
||||
return (rval >= vbox.r1 && rval <= vbox.r2 &&
|
||||
gval >= vbox.g1 && gval <= vbox.g2 &&
|
||||
bval >= vbox.b1 && bval <= vbox.b2);
|
||||
}
|
||||
};
|
||||
|
||||
// Color map
|
||||
function CMap() {
|
||||
this.vboxes = new PQueue(function(a,b) {
|
||||
return pv.naturalOrder(
|
||||
a.vbox.count()*a.vbox.volume(),
|
||||
b.vbox.count()*b.vbox.volume()
|
||||
);
|
||||
});
|
||||
}
|
||||
CMap.prototype = {
|
||||
push: function(vbox) {
|
||||
this.vboxes.push({
|
||||
vbox: vbox,
|
||||
color: vbox.avg()
|
||||
});
|
||||
},
|
||||
palette: function() {
|
||||
return this.vboxes.map(function(vb) { return vb.color; });
|
||||
},
|
||||
size: function() {
|
||||
return this.vboxes.size();
|
||||
},
|
||||
map: function(color) {
|
||||
var vboxes = this.vboxes;
|
||||
for (var i=0; i<vboxes.size(); i++) {
|
||||
if (vboxes.peek(i).vbox.contains(color)) {
|
||||
return vboxes.peek(i).color;
|
||||
}
|
||||
}
|
||||
return this.nearest(color);
|
||||
},
|
||||
nearest: function(color) {
|
||||
var vboxes = this.vboxes,
|
||||
d1, d2, pColor;
|
||||
for (var i=0; i<vboxes.size(); i++) {
|
||||
d2 = Math.sqrt(
|
||||
Math.pow(color[0] - vboxes.peek(i).color[0], 2) +
|
||||
Math.pow(color[1] - vboxes.peek(i).color[1], 2) +
|
||||
Math.pow(color[2] - vboxes.peek(i).color[2], 2)
|
||||
);
|
||||
if (d2 < d1 || d1 === undefined) {
|
||||
d1 = d2;
|
||||
pColor = vboxes.peek(i).color;
|
||||
}
|
||||
}
|
||||
return pColor;
|
||||
},
|
||||
forcebw: function() {
|
||||
// XXX: won't work yet
|
||||
var vboxes = this.vboxes;
|
||||
vboxes.sort(function(a,b) { return pv.naturalOrder(pv.sum(a.color), pv.sum(b.color));});
|
||||
|
||||
// force darkest color to black if everything < 5
|
||||
var lowest = vboxes[0].color;
|
||||
if (lowest[0] < 5 && lowest[1] < 5 && lowest[2] < 5)
|
||||
vboxes[0].color = [0,0,0];
|
||||
|
||||
// force lightest color to white if everything > 251
|
||||
var idx = vboxes.length-1,
|
||||
highest = vboxes[idx].color;
|
||||
if (highest[0] > 251 && highest[1] > 251 && highest[2] > 251)
|
||||
vboxes[idx].color = [255,255,255];
|
||||
}
|
||||
};
|
||||
|
||||
// histo (1-d array, giving the number of pixels in
|
||||
// each quantized region of color space), or null on error
|
||||
function getHisto(pixels) {
|
||||
var histosize = 1 << (3 * sigbits),
|
||||
histo = new Array(histosize),
|
||||
index, rval, gval, bval;
|
||||
pixels.forEach(function(pixel) {
|
||||
rval = pixel[0] >> rshift;
|
||||
gval = pixel[1] >> rshift;
|
||||
bval = pixel[2] >> rshift;
|
||||
index = getColorIndex(rval, gval, bval);
|
||||
histo[index] = (histo[index] || 0) + 1;
|
||||
});
|
||||
return histo;
|
||||
}
|
||||
|
||||
function vboxFromPixels(pixels, histo) {
|
||||
var rmin=1000000, rmax=0,
|
||||
gmin=1000000, gmax=0,
|
||||
bmin=1000000, bmax=0,
|
||||
rval, gval, bval;
|
||||
// find min/max
|
||||
pixels.forEach(function(pixel) {
|
||||
rval = pixel[0] >> rshift;
|
||||
gval = pixel[1] >> rshift;
|
||||
bval = pixel[2] >> rshift;
|
||||
if (rval < rmin) rmin = rval;
|
||||
else if (rval > rmax) rmax = rval;
|
||||
if (gval < gmin) gmin = gval;
|
||||
else if (gval > gmax) gmax = gval;
|
||||
if (bval < bmin) bmin = bval;
|
||||
else if (bval > bmax) bmax = bval;
|
||||
});
|
||||
return new VBox(rmin, rmax, gmin, gmax, bmin, bmax, histo);
|
||||
}
|
||||
|
||||
function medianCutApply(histo, vbox) {
|
||||
if (!vbox.count()) return;
|
||||
|
||||
var rw = vbox.r2 - vbox.r1 + 1,
|
||||
gw = vbox.g2 - vbox.g1 + 1,
|
||||
bw = vbox.b2 - vbox.b1 + 1,
|
||||
maxw = pv.max([rw, gw, bw]);
|
||||
// only one pixel, no split
|
||||
if (vbox.count() == 1) {
|
||||
return [vbox.copy()];
|
||||
}
|
||||
/* Find the partial sum arrays along the selected axis. */
|
||||
var total = 0,
|
||||
partialsum = [],
|
||||
lookaheadsum = [],
|
||||
i, j, k, sum, index;
|
||||
if (maxw == rw) {
|
||||
for (i = vbox.r1; i <= vbox.r2; i++) {
|
||||
sum = 0;
|
||||
for (j = vbox.g1; j <= vbox.g2; j++) {
|
||||
for (k = vbox.b1; k <= vbox.b2; k++) {
|
||||
index = getColorIndex(i,j,k);
|
||||
sum += (histo[index] || 0);
|
||||
}
|
||||
}
|
||||
total += sum;
|
||||
partialsum[i] = total;
|
||||
}
|
||||
}
|
||||
else if (maxw == gw) {
|
||||
for (i = vbox.g1; i <= vbox.g2; i++) {
|
||||
sum = 0;
|
||||
for (j = vbox.r1; j <= vbox.r2; j++) {
|
||||
for (k = vbox.b1; k <= vbox.b2; k++) {
|
||||
index = getColorIndex(j,i,k);
|
||||
sum += (histo[index] || 0);
|
||||
}
|
||||
}
|
||||
total += sum;
|
||||
partialsum[i] = total;
|
||||
}
|
||||
}
|
||||
else { /* maxw == bw */
|
||||
for (i = vbox.b1; i <= vbox.b2; i++) {
|
||||
sum = 0;
|
||||
for (j = vbox.r1; j <= vbox.r2; j++) {
|
||||
for (k = vbox.g1; k <= vbox.g2; k++) {
|
||||
index = getColorIndex(j,k,i);
|
||||
sum += (histo[index] || 0);
|
||||
}
|
||||
}
|
||||
total += sum;
|
||||
partialsum[i] = total;
|
||||
}
|
||||
}
|
||||
partialsum.forEach(function(d,i) {
|
||||
lookaheadsum[i] = total-d;
|
||||
});
|
||||
function doCut(color) {
|
||||
var dim1 = color + '1',
|
||||
dim2 = color + '2',
|
||||
left, right, vbox1, vbox2, d2, count2=0;
|
||||
for (i = vbox[dim1]; i <= vbox[dim2]; i++) {
|
||||
if (partialsum[i] > total / 2) {
|
||||
vbox1 = vbox.copy();
|
||||
vbox2 = vbox.copy();
|
||||
left = i - vbox[dim1];
|
||||
right = vbox[dim2] - i;
|
||||
if (left <= right)
|
||||
d2 = Math.min(vbox[dim2] - 1, ~~(i + right / 2));
|
||||
else d2 = Math.max(vbox[dim1], ~~(i - 1 - left / 2));
|
||||
// avoid 0-count boxes
|
||||
while (!partialsum[d2]) d2++;
|
||||
count2 = lookaheadsum[d2];
|
||||
while (!count2 && partialsum[d2-1]) count2 = lookaheadsum[--d2];
|
||||
// set dimensions
|
||||
vbox1[dim2] = d2;
|
||||
vbox2[dim1] = vbox1[dim2] + 1;
|
||||
// console.log('vbox counts:', vbox.count(), vbox1.count(), vbox2.count());
|
||||
return [vbox1, vbox2];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// determine the cut planes
|
||||
return maxw == rw ? doCut('r') :
|
||||
maxw == gw ? doCut('g') :
|
||||
doCut('b');
|
||||
}
|
||||
|
||||
function quantize(pixels, maxcolors) {
|
||||
// short-circuit
|
||||
if (!pixels.length || maxcolors < 2 || maxcolors > 256) {
|
||||
// console.log('wrong number of maxcolors');
|
||||
return false;
|
||||
}
|
||||
|
||||
// XXX: check color content and convert to grayscale if insufficient
|
||||
|
||||
var histo = getHisto(pixels),
|
||||
histosize = 1 << (3 * sigbits);
|
||||
|
||||
// check that we aren't below maxcolors already
|
||||
var nColors = 0;
|
||||
histo.forEach(function() { nColors++; });
|
||||
if (nColors <= maxcolors) {
|
||||
// XXX: generate the new colors from the histo and return
|
||||
}
|
||||
|
||||
// get the beginning vbox from the colors
|
||||
var vbox = vboxFromPixels(pixels, histo),
|
||||
pq = new PQueue(function(a,b) { return pv.naturalOrder(a.count(), b.count()); });
|
||||
pq.push(vbox);
|
||||
|
||||
// inner function to do the iteration
|
||||
function iter(lh, target) {
|
||||
var ncolors = 1,
|
||||
niters = 0,
|
||||
vbox;
|
||||
while (niters < maxIterations) {
|
||||
vbox = lh.pop();
|
||||
if (!vbox.count()) { /* just put it back */
|
||||
lh.push(vbox);
|
||||
niters++;
|
||||
continue;
|
||||
}
|
||||
// do the cut
|
||||
var vboxes = medianCutApply(histo, vbox),
|
||||
vbox1 = vboxes[0],
|
||||
vbox2 = vboxes[1];
|
||||
|
||||
if (!vbox1) {
|
||||
// console.log("vbox1 not defined; shouldn't happen!");
|
||||
return;
|
||||
}
|
||||
lh.push(vbox1);
|
||||
if (vbox2) { /* vbox2 can be null */
|
||||
lh.push(vbox2);
|
||||
ncolors++;
|
||||
}
|
||||
if (ncolors >= target) return;
|
||||
if (niters++ > maxIterations) {
|
||||
// console.log("infinite loop; perhaps too few pixels!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// first set of colors, sorted by population
|
||||
iter(pq, fractByPopulations * maxcolors);
|
||||
|
||||
// Re-sort by the product of pixel occupancy times the size in color space.
|
||||
var pq2 = new PQueue(function(a,b) {
|
||||
return pv.naturalOrder(a.count()*a.volume(), b.count()*b.volume());
|
||||
});
|
||||
while (pq.size()) {
|
||||
pq2.push(pq.pop());
|
||||
}
|
||||
|
||||
// next set - generate the median cuts using the (npix * vol) sorting.
|
||||
iter(pq2, maxcolors - pq2.size());
|
||||
|
||||
// calculate the actual colors
|
||||
var cmap = new CMap();
|
||||
while (pq2.size()) {
|
||||
cmap.push(pq2.pop());
|
||||
}
|
||||
|
||||
return cmap;
|
||||
}
|
||||
|
||||
return {
|
||||
quantize: quantize
|
||||
};
|
||||
})();
|
||||
258
gaia/distros/spark/apps/studio/js/generation.js
Executable file
258
gaia/distros/spark/apps/studio/js/generation.js
Executable file
|
|
@ -0,0 +1,258 @@
|
|||
/*global
|
||||
MimeMapper
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
function proposeDownload(blob) {
|
||||
console.log('proposeDownload');
|
||||
var url = window.URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'app.zip';
|
||||
a.hidden = true;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(blob);
|
||||
}
|
||||
|
||||
function uninstallIfNeeded(theme) {
|
||||
if (!theme.manifestURL) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
var all = navigator.mozApps.mgmt.getAll();
|
||||
all.onsuccess = (e) => {
|
||||
var toDelete = e.target.result.find((app) => {
|
||||
return (app.manifestURL === theme.manifestURL);
|
||||
});
|
||||
if (toDelete) {
|
||||
var req = navigator.mozApps.mgmt.uninstall(toDelete);
|
||||
req.onsuccess = resolve;
|
||||
req.onerror = reject;
|
||||
} else {
|
||||
reject(new Error('could not find the app to uninstall'));
|
||||
}
|
||||
};
|
||||
all.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
function importBlob(blob, enable) {
|
||||
console.log('importBlob');
|
||||
if (!navigator.getDeviceStorage) {
|
||||
return Promise.reject(new Error('getDeviceStorage is unavailable.'));
|
||||
}
|
||||
// Save the blob to a file because we don't support importing memory blobs.
|
||||
var sdcard = navigator.getDeviceStorage('sdcard');
|
||||
return new Promise((resolve, reject) => {
|
||||
function sendError(msg) {
|
||||
console.log(msg);
|
||||
reject(msg);
|
||||
}
|
||||
|
||||
if (!sdcard) {
|
||||
sendError('No SDCard!');
|
||||
return;
|
||||
}
|
||||
|
||||
var fileName = 'temp-app.zip';
|
||||
|
||||
var delReq = sdcard.delete(fileName);
|
||||
delReq.onsuccess = delReq.onerror = function() {
|
||||
var req = sdcard.addNamed(blob, fileName);
|
||||
req.onsuccess = function(e) {
|
||||
var getReq = sdcard.get(fileName);
|
||||
getReq.onsuccess = function() {
|
||||
var file = this.result;
|
||||
navigator.mozApps.mgmt.import(file).then(
|
||||
function(app) {
|
||||
if (enable) {
|
||||
var setting = { 'theme.selected' : app.manifestURL };
|
||||
var req = navigator.mozSettings.createLock().set(setting);
|
||||
req.onsuccess = resolve.bind(null, app.manifestURL);
|
||||
req.onerror = sendError;
|
||||
} else {
|
||||
resolve(app.manifestURL);
|
||||
}
|
||||
},
|
||||
function(error) { sendError('Error importing: ' + error.name); }
|
||||
);
|
||||
}
|
||||
getReq.onerror = function() {
|
||||
sendError('Error getting file: ' + this.error.name);
|
||||
}
|
||||
}
|
||||
req.onerror = function(e) {
|
||||
sendError('Error saving blob: ' + this.error.name);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function exportTheme(enable, theme) {
|
||||
// Worker path configuration.
|
||||
zip.workerScriptsPath = './js/libs/';
|
||||
|
||||
console.log('exportTheme ' + theme.title);
|
||||
|
||||
// TODO: use a stable appId.
|
||||
var appId = 'app' + Math.round(Math.random() * 100000000);
|
||||
var app = new AppZip();
|
||||
app.metaData = {
|
||||
installOrigin: 'http://gaiamobile.org',
|
||||
manifestURL: 'app://theme' + appId + '.gaiamobile.org/update.webapp',
|
||||
version: 1
|
||||
};
|
||||
|
||||
app.manifest = {
|
||||
name: theme.title,
|
||||
package_path: '/application.zip',
|
||||
};
|
||||
|
||||
// Build a simple package with a manifest and index.html
|
||||
var inner = new PackageHelper();
|
||||
|
||||
var manifest = {
|
||||
name: theme.title,
|
||||
role: "theme",
|
||||
type: "certified",
|
||||
origin: 'app://theme' + appId + '.gaiamobile.org'
|
||||
};
|
||||
|
||||
return inner.init().then(() => {
|
||||
function addManifest() {
|
||||
return inner.addResource('manifest.webapp', JSON.stringify(manifest));
|
||||
}
|
||||
|
||||
function addCSS() {
|
||||
return inner.addResource('shared/elements/gaia-theme/gaia-theme.css', userStyle(theme));
|
||||
}
|
||||
|
||||
function addWallpaper(blob) {
|
||||
// if we don't have a (valid) wallpaper we still want an empty JSON
|
||||
var wallpaperFile = null;
|
||||
var wallpaper = { homescreen: null };
|
||||
if (blob) {
|
||||
var type = MimeMapper.guessExtensionFromType(blob.type);
|
||||
if (type) {
|
||||
wallpaperFile = 'wallpaper.' + type;
|
||||
wallpaper.homescreen = '/' + wallpaperFile;
|
||||
} else {
|
||||
console.error('Unrecognized type: ' + blob.type);
|
||||
}
|
||||
}
|
||||
|
||||
// We can't use Promise.all to add both files because it corrupts the
|
||||
// zip
|
||||
return Promise.resolve()
|
||||
.then(() => inner.addResource(
|
||||
'wallpaper.json', JSON.stringify(wallpaper)))
|
||||
.then(function() {
|
||||
if (wallpaperFile) {
|
||||
inner.addResource(wallpaperFile, blob);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var zipPromise = addManifest().then(addCSS);
|
||||
var image = null;
|
||||
if (theme.autotheme) {
|
||||
image = theme.autotheme.image;
|
||||
}
|
||||
zipPromise = zipPromise.then(() => addWallpaper(image));
|
||||
|
||||
return zipPromise.then(inner.asBlob.bind(inner))
|
||||
.then((packageBlob) => {
|
||||
app.packageblob = packageBlob;
|
||||
return app.asBlob();
|
||||
}).then((appBlob) => {
|
||||
return uninstallIfNeeded(theme)
|
||||
.then(() => {
|
||||
return importBlob(appBlob, enable);
|
||||
})
|
||||
.then((manifestURL) => {
|
||||
theme.manifestURL = manifestURL;
|
||||
return Storage.updateTheme(theme);
|
||||
})
|
||||
.then(() => Promise.resolve(image &&
|
||||
navigator.mozSettings.createLock().set({
|
||||
'wallpaper.image': image
|
||||
})
|
||||
))
|
||||
.catch((e) => {
|
||||
console.error('Error while importing blob', e);
|
||||
// Desktop debug
|
||||
// proposeDownload(appBlob);
|
||||
alert('This device is not theming compatible');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function userStyle(theme) {
|
||||
var roots = [':root', '.theme-productivity', '.theme-communications',
|
||||
'.theme-settings', '.theme-media'];
|
||||
|
||||
var str = '';
|
||||
roots.forEach(function(root) {
|
||||
str += root + ' {\n';
|
||||
var group;
|
||||
if (root === ':root') {
|
||||
group = 'default';
|
||||
// Add :root CSS variables here.
|
||||
str += '--transition-duration: 0.2s;\n';
|
||||
} else {
|
||||
group = root.split('-')[1];
|
||||
}
|
||||
Object.keys(theme.groups[group]).forEach(function(sectionKey) {
|
||||
var section = theme.groups[group][sectionKey];
|
||||
Object.keys(section).forEach(function(key) {
|
||||
str += key + ': ' + section[key] + ';\n';
|
||||
});
|
||||
});
|
||||
str += '}\n';
|
||||
});
|
||||
return str;
|
||||
}
|
||||
|
||||
function fakeInstall(css) {
|
||||
var override = document.createElement('style');
|
||||
override.innerHTML = css;
|
||||
document.body.appendChild(override);
|
||||
}
|
||||
|
||||
function checkIfSelected(theme) {
|
||||
return new Promise((resolve, reject) => {
|
||||
var req = navigator.mozSettings.createLock().get('theme.selected');
|
||||
req.onsuccess = function(e) {
|
||||
resolve([(req.result['theme.selected'] == theme.manifestURL), theme]);
|
||||
};
|
||||
req.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
exports.Generation = {
|
||||
installTheme: function(id, enable) {
|
||||
return Storage.fetchTheme(id)
|
||||
.then(checkIfSelected)
|
||||
.then((results) => {
|
||||
return exportTheme(results[0] || enable, results[1]);
|
||||
});
|
||||
},
|
||||
uninstallIfNeeded: function(theme) {
|
||||
return checkIfSelected(theme)
|
||||
.then((results) => {
|
||||
var selected = results[0];
|
||||
if (selected) {
|
||||
alert("You can't remove the selected theme");
|
||||
} else {
|
||||
return uninstallIfNeeded(theme);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
})(window);
|
||||
149
gaia/distros/spark/apps/studio/js/libs/appzip.js
Executable file
149
gaia/distros/spark/apps/studio/js/libs/appzip.js
Executable file
|
|
@ -0,0 +1,149 @@
|
|||
|
||||
(function(global) {
|
||||
'use strict';
|
||||
|
||||
// Promise based version of some zip functions.
|
||||
function writerAdd(writer, filename, data, func) {
|
||||
return new Promise((resolve, reject) => {
|
||||
writer.add(filename, new func(data),
|
||||
function() {
|
||||
resolve();
|
||||
},
|
||||
function(currentIndex, totalIndex) {
|
||||
// onprogress callback, ununsed.
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to easily create zips as Blobs.
|
||||
* Usage:
|
||||
* var zip = new PackageHelper();
|
||||
* zip.init().then(() => {
|
||||
* zip.addResource('metadata.json', JSON.parse(this._meta));
|
||||
* zip.addResource('application.zip', blob);
|
||||
* zip.asBlob(success, console.error);
|
||||
* }, console.error);
|
||||
*/
|
||||
function PackageHelper() {
|
||||
this._writer = null;
|
||||
}
|
||||
|
||||
PackageHelper.prototype = {
|
||||
init: function() {
|
||||
dump('PackageHelper.init()');
|
||||
var self = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
zip.createWriter(new zip.BlobWriter(),
|
||||
function(writer) {
|
||||
self._writer = writer;
|
||||
resolve();
|
||||
}, reject);
|
||||
})
|
||||
},
|
||||
|
||||
addResource: function(path, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this._writer) {
|
||||
reject();
|
||||
}
|
||||
var func = typeof data === 'object' ? zip.BlobReader : zip.TextReader;
|
||||
writerAdd(this._writer, path, data, func).then(resolve, reject);
|
||||
});
|
||||
},
|
||||
|
||||
asBlob: function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this._writer.close(function(blob) {
|
||||
// blob contains the zip file as a Blob object
|
||||
resolve(blob);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to create an exportable app Blob.
|
||||
*/
|
||||
function AppZip() {
|
||||
this._meta = null;
|
||||
this._package = null;
|
||||
this._manifest = null;
|
||||
}
|
||||
|
||||
AppZip.prototype = {
|
||||
|
||||
// Meta is a JSON object like:
|
||||
// {
|
||||
// installOrigin: ...,
|
||||
// manifestURL: ...,
|
||||
// version: 1
|
||||
// };
|
||||
set metaData(meta) {
|
||||
if (meta &&
|
||||
meta.installOrigin &&
|
||||
meta.manifestURL &&
|
||||
meta.version == 1) {
|
||||
this._meta = meta;
|
||||
}
|
||||
},
|
||||
|
||||
// Package needs to be a zip blob.
|
||||
set packageblob(pack) {
|
||||
this._package = pack;
|
||||
},
|
||||
|
||||
// The JSON manifest. For hosted apps this is the app manifest, for
|
||||
// packaged apps this is the mini manifest.
|
||||
set manifest(manifest) {
|
||||
this._manifest = manifest;
|
||||
},
|
||||
|
||||
// Returns a Promise that resolves to a Blob that can be imported as an
|
||||
// app.
|
||||
asBlob: function() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this._meta) {
|
||||
reject('NoMetaData');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._manifest) {
|
||||
reject('NoManifest');
|
||||
return;
|
||||
}
|
||||
|
||||
var manifestPath = this._package ? 'update.webapp' : 'manifest.webapp';
|
||||
var zip = new PackageHelper();
|
||||
var self = this;
|
||||
zip.init().then(() => {
|
||||
function addMeta() {
|
||||
return zip.addResource('metadata.json', JSON.stringify(self._meta));
|
||||
}
|
||||
|
||||
function addManifest() {
|
||||
return zip.addResource(manifestPath, JSON.stringify(self._manifest));
|
||||
}
|
||||
|
||||
function addPackage() {
|
||||
if (self._package) {
|
||||
return zip.addResource('application.zip', self._package);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return addMeta().then(addManifest)
|
||||
.then(addPackage)
|
||||
.then(zip.asBlob.bind(zip))
|
||||
.then(resolve);
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
global.PackageHelper = PackageHelper;
|
||||
global.AppZip = AppZip;
|
||||
})(window);
|
||||
2060
gaia/distros/spark/apps/studio/js/libs/deflate.js
Executable file
2060
gaia/distros/spark/apps/studio/js/libs/deflate.js
Executable file
File diff suppressed because it is too large
Load diff
1571
gaia/distros/spark/apps/studio/js/libs/l10n.js
Executable file
1571
gaia/distros/spark/apps/studio/js/libs/l10n.js
Executable file
File diff suppressed because it is too large
Load diff
1002
gaia/distros/spark/apps/studio/js/libs/mime-types.js
Executable file
1002
gaia/distros/spark/apps/studio/js/libs/mime-types.js
Executable file
File diff suppressed because it is too large
Load diff
135
gaia/distros/spark/apps/studio/js/libs/mime_mapper.js
Executable file
135
gaia/distros/spark/apps/studio/js/libs/mime_mapper.js
Executable file
|
|
@ -0,0 +1,135 @@
|
|||
/* exported MimeMapper */
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* MimeMapper helps gaia apps to decide the mapping of mimetype and extension.
|
||||
* The use cases often happen when apps need to know about the exact
|
||||
* mimetypes or extensions, such as to delegate the open web activity, we must
|
||||
* have suitable mimetypes or extensions to request the right activity
|
||||
*
|
||||
* The mapping is basically created according to:
|
||||
* http://en.wikipedia.org/wiki/Internet_media_type
|
||||
*
|
||||
* The supported formats are considered base on the deviceStorage properties:
|
||||
* http://dxr.mozilla.org/mozilla-central/toolkit/content/
|
||||
* devicestorage.properties
|
||||
*
|
||||
*/
|
||||
|
||||
var MimeMapper = {
|
||||
// This list only contains the extensions we currently supported
|
||||
// We should make it more complete for further usages
|
||||
_typeToExtensionMap: {
|
||||
// Image
|
||||
'image/jpeg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/gif': 'gif',
|
||||
'image/bmp': 'bmp',
|
||||
// Audio
|
||||
'audio/mpeg': 'mp3',
|
||||
'audio/mp4': 'm4a',
|
||||
'audio/ogg': 'ogg',
|
||||
'audio/webm': 'webm',
|
||||
'audio/3gpp': '3gp',
|
||||
'audio/amr': 'amr',
|
||||
// Video
|
||||
'video/mp4': 'mp4',
|
||||
'video/mpeg': 'mpg',
|
||||
'video/ogg': 'ogg',
|
||||
'video/webm': 'webm',
|
||||
'video/3gpp': '3gp',
|
||||
'video/3gpp2': '3g2',
|
||||
// Application
|
||||
// If we want to support some types, like pdf, just add
|
||||
// 'application/pdf': 'pdf'
|
||||
'application/vcard': 'vcf',
|
||||
// Text
|
||||
'text/vcard': 'vcf',
|
||||
'text/x-vcard': 'vcf'
|
||||
},
|
||||
|
||||
// This list only contains the mimetypes we currently supported
|
||||
// We should make it more complete for further usages
|
||||
_extensionToTypeMap: {
|
||||
// Image
|
||||
'jpg': 'image/jpeg',
|
||||
'jpeg': 'image/jpeg',
|
||||
'jpe': 'image/jpeg',
|
||||
'png': 'image/png',
|
||||
'gif': 'image/gif',
|
||||
'bmp': 'image/bmp',
|
||||
// Audio
|
||||
'mp3': 'audio/mpeg',
|
||||
'm4a': 'audio/mp4',
|
||||
'm4b': 'audio/mp4',
|
||||
'm4p': 'audio/mp4',
|
||||
'm4r': 'audio/mp4',
|
||||
'aac': 'audio/aac',
|
||||
'opus': 'audio/ogg',
|
||||
'amr': 'audio/amr',
|
||||
// Video
|
||||
'mp4': 'video/mp4',
|
||||
'mpeg': 'video/mpeg',
|
||||
'mpg': 'video/mpeg',
|
||||
'ogv': 'video/ogg',
|
||||
'ogx': 'video/ogg',
|
||||
'webm': 'video/webm',
|
||||
'3gp': 'video/3gpp',
|
||||
'3g2': 'video/3gpp2',
|
||||
'ogg': 'video/ogg',
|
||||
// Application
|
||||
// If we want to support some extensions, like pdf, just add
|
||||
// 'pdf': 'application/pdf'
|
||||
// Text
|
||||
'vcf': 'text/vcard'
|
||||
},
|
||||
_parseExtension: function(filename) {
|
||||
var array = filename.split('.');
|
||||
return array.length > 1 ? array.pop() : '';
|
||||
},
|
||||
|
||||
isSupportedType: function(mimetype) {
|
||||
return (mimetype in this._typeToExtensionMap);
|
||||
},
|
||||
|
||||
isSupportedExtension: function(extension) {
|
||||
return (extension in this._extensionToTypeMap);
|
||||
},
|
||||
|
||||
isFilenameMatchesType: function(filename, mimetype) {
|
||||
var extension = this._parseExtension(filename);
|
||||
var guessedType = this.guessTypeFromExtension(extension);
|
||||
return (guessedType == mimetype);
|
||||
},
|
||||
|
||||
guessExtensionFromType: function(mimetype) {
|
||||
return this._typeToExtensionMap[mimetype];
|
||||
},
|
||||
|
||||
guessTypeFromExtension: function(extension) {
|
||||
return this._extensionToTypeMap[extension];
|
||||
},
|
||||
|
||||
// If mimetype is not in the supported list, we will try to
|
||||
// predict the possible valid mimetype based on extension.
|
||||
guessTypeFromFileProperties: function(filename, mimetype) {
|
||||
var extension = this._parseExtension(filename);
|
||||
var type = this.isSupportedType(mimetype) ?
|
||||
mimetype : this.guessTypeFromExtension(extension);
|
||||
return type || '';
|
||||
},
|
||||
|
||||
// if mimetype is not supported, preserve the original extension
|
||||
// and add the predict result as new extension.
|
||||
// If both filename and mimetype are not supported, return the original
|
||||
// filename.
|
||||
ensureFilenameMatchesType: function(filename, mimetype) {
|
||||
if (!this.isFilenameMatchesType(filename, mimetype)) {
|
||||
var guessedExt = this.guessExtensionFromType(mimetype);
|
||||
if (guessedExt) {
|
||||
filename += '.' + guessedExt;
|
||||
}
|
||||
}
|
||||
return filename;
|
||||
}
|
||||
};
|
||||
147
gaia/distros/spark/apps/studio/js/libs/z-worker.js
Executable file
147
gaia/distros/spark/apps/studio/js/libs/z-worker.js
Executable file
|
|
@ -0,0 +1,147 @@
|
|||
/* jshint worker:true */
|
||||
(function main(global) {
|
||||
"use strict";
|
||||
|
||||
if (global.zWorkerInitialized)
|
||||
throw new Error('z-worker.js should be run only once');
|
||||
global.zWorkerInitialized = true;
|
||||
|
||||
addEventListener("message", function(event) {
|
||||
var message = event.data, type = message.type, sn = message.sn;
|
||||
var handler = handlers[type];
|
||||
if (handler) {
|
||||
try {
|
||||
handler(message);
|
||||
} catch (e) {
|
||||
onError(type, sn, e);
|
||||
}
|
||||
}
|
||||
//for debug
|
||||
//postMessage({type: 'echo', originalType: type, sn: sn});
|
||||
});
|
||||
|
||||
var handlers = {
|
||||
importScripts: doImportScripts,
|
||||
newTask: newTask,
|
||||
append: processData,
|
||||
flush: processData,
|
||||
};
|
||||
|
||||
// deflater/inflater tasks indexed by serial numbers
|
||||
var tasks = {};
|
||||
|
||||
function doImportScripts(msg) {
|
||||
if (msg.scripts && msg.scripts.length > 0)
|
||||
importScripts.apply(undefined, msg.scripts);
|
||||
postMessage({type: 'importScripts'});
|
||||
}
|
||||
|
||||
function newTask(msg) {
|
||||
var CodecClass = global[msg.codecClass];
|
||||
var sn = msg.sn;
|
||||
if (tasks[sn])
|
||||
throw Error('duplicated sn');
|
||||
tasks[sn] = {
|
||||
codec: new CodecClass(msg.options),
|
||||
crcInput: msg.crcType === 'input',
|
||||
crcOutput: msg.crcType === 'output',
|
||||
crc: new Crc32(),
|
||||
};
|
||||
postMessage({type: 'newTask', sn: sn});
|
||||
}
|
||||
|
||||
// performance may not be supported
|
||||
var now = global.performance ? global.performance.now.bind(global.performance) : Date.now;
|
||||
|
||||
function processData(msg) {
|
||||
var sn = msg.sn, type = msg.type, input = msg.data;
|
||||
var task = tasks[sn];
|
||||
// allow creating codec on first append
|
||||
if (!task && msg.codecClass) {
|
||||
newTask(msg);
|
||||
task = tasks[sn];
|
||||
}
|
||||
var isAppend = type === 'append';
|
||||
var start = now();
|
||||
var output;
|
||||
if (isAppend) {
|
||||
try {
|
||||
output = task.codec.append(input, function onprogress(loaded) {
|
||||
postMessage({type: 'progress', sn: sn, loaded: loaded});
|
||||
});
|
||||
} catch (e) {
|
||||
delete tasks[sn];
|
||||
throw e;
|
||||
}
|
||||
} else {
|
||||
delete tasks[sn];
|
||||
output = task.codec.flush();
|
||||
}
|
||||
var codecTime = now() - start;
|
||||
|
||||
start = now();
|
||||
if (input && task.crcInput)
|
||||
task.crc.append(input);
|
||||
if (output && task.crcOutput)
|
||||
task.crc.append(output);
|
||||
var crcTime = now() - start;
|
||||
|
||||
var rmsg = {type: type, sn: sn, codecTime: codecTime, crcTime: crcTime};
|
||||
var transferables = [];
|
||||
if (output) {
|
||||
rmsg.data = output;
|
||||
transferables.push(output.buffer);
|
||||
}
|
||||
if (!isAppend && (task.crcInput || task.crcOutput))
|
||||
rmsg.crc = task.crc.get();
|
||||
postMessage(rmsg, transferables);
|
||||
}
|
||||
|
||||
function onError(type, sn, e) {
|
||||
var msg = {
|
||||
type: type,
|
||||
sn: sn,
|
||||
error: formatError(e)
|
||||
};
|
||||
postMessage(msg);
|
||||
}
|
||||
|
||||
function formatError(e) {
|
||||
return { message: e.message, stack: e.stack };
|
||||
}
|
||||
|
||||
// Crc32 code copied from file zip.js
|
||||
function Crc32() {
|
||||
this.crc = -1;
|
||||
}
|
||||
Crc32.prototype.append = function append(data) {
|
||||
var crc = this.crc | 0, table = this.table;
|
||||
for (var offset = 0, len = data.length | 0; offset < len; offset++)
|
||||
crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF];
|
||||
this.crc = crc;
|
||||
};
|
||||
Crc32.prototype.get = function get() {
|
||||
return ~this.crc;
|
||||
};
|
||||
Crc32.prototype.table = (function() {
|
||||
var i, j, t, table = []; // Uint32Array is actually slower than []
|
||||
for (i = 0; i < 256; i++) {
|
||||
t = i;
|
||||
for (j = 0; j < 8; j++)
|
||||
if (t & 1)
|
||||
t = (t >>> 1) ^ 0xEDB88320;
|
||||
else
|
||||
t = t >>> 1;
|
||||
table[i] = t;
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
|
||||
// "no-op" codec
|
||||
function NOOP() {}
|
||||
global.NOOP = NOOP;
|
||||
NOOP.prototype.append = function append(bytes, onprogress) {
|
||||
return bytes;
|
||||
};
|
||||
NOOP.prototype.flush = function flush() {};
|
||||
})(this);
|
||||
959
gaia/distros/spark/apps/studio/js/libs/zip.js
Executable file
959
gaia/distros/spark/apps/studio/js/libs/zip.js
Executable file
|
|
@ -0,0 +1,959 @@
|
|||
/*
|
||||
Copyright (c) 2013 Gildas Lormeau. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in
|
||||
the documentation and/or other materials provided with the distribution.
|
||||
|
||||
3. The names of the authors may not be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
|
||||
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JCRAFT,
|
||||
INC. OR ANY CONTRIBUTORS TO THIS SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
|
||||
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
||||
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
(function(obj) {
|
||||
"use strict";
|
||||
|
||||
var ERR_BAD_FORMAT = "File format is not recognized.";
|
||||
var ERR_CRC = "CRC failed.";
|
||||
var ERR_ENCRYPTED = "File contains encrypted entry.";
|
||||
var ERR_ZIP64 = "File is using Zip64 (4gb+ file size).";
|
||||
var ERR_READ = "Error while reading zip file.";
|
||||
var ERR_WRITE = "Error while writing zip file.";
|
||||
var ERR_WRITE_DATA = "Error while writing file data.";
|
||||
var ERR_READ_DATA = "Error while reading file data.";
|
||||
var ERR_DUPLICATED_NAME = "File already exists.";
|
||||
var CHUNK_SIZE = 512 * 1024;
|
||||
|
||||
var TEXT_PLAIN = "text/plain";
|
||||
|
||||
var appendABViewSupported;
|
||||
try {
|
||||
appendABViewSupported = new Blob([ new DataView(new ArrayBuffer(0)) ]).size === 0;
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
function Crc32() {
|
||||
this.crc = -1;
|
||||
}
|
||||
Crc32.prototype.append = function append(data) {
|
||||
var crc = this.crc | 0, table = this.table;
|
||||
for (var offset = 0, len = data.length | 0; offset < len; offset++)
|
||||
crc = (crc >>> 8) ^ table[(crc ^ data[offset]) & 0xFF];
|
||||
this.crc = crc;
|
||||
};
|
||||
Crc32.prototype.get = function get() {
|
||||
return ~this.crc;
|
||||
};
|
||||
Crc32.prototype.table = (function() {
|
||||
var i, j, t, table = []; // Uint32Array is actually slower than []
|
||||
for (i = 0; i < 256; i++) {
|
||||
t = i;
|
||||
for (j = 0; j < 8; j++)
|
||||
if (t & 1)
|
||||
t = (t >>> 1) ^ 0xEDB88320;
|
||||
else
|
||||
t = t >>> 1;
|
||||
table[i] = t;
|
||||
}
|
||||
return table;
|
||||
})();
|
||||
|
||||
// "no-op" codec
|
||||
function NOOP() {}
|
||||
NOOP.prototype.append = function append(bytes, onprogress) {
|
||||
return bytes;
|
||||
};
|
||||
NOOP.prototype.flush = function flush() {};
|
||||
|
||||
function blobSlice(blob, index, length) {
|
||||
if (index < 0 || length < 0 || index + length > blob.size)
|
||||
throw new RangeError('offset:' + index + ', length:' + length + ', size:' + blob.size);
|
||||
if (blob.slice)
|
||||
return blob.slice(index, index + length);
|
||||
else if (blob.webkitSlice)
|
||||
return blob.webkitSlice(index, index + length);
|
||||
else if (blob.mozSlice)
|
||||
return blob.mozSlice(index, index + length);
|
||||
else if (blob.msSlice)
|
||||
return blob.msSlice(index, index + length);
|
||||
}
|
||||
|
||||
function getDataHelper(byteLength, bytes) {
|
||||
var dataBuffer, dataArray;
|
||||
dataBuffer = new ArrayBuffer(byteLength);
|
||||
dataArray = new Uint8Array(dataBuffer);
|
||||
if (bytes)
|
||||
dataArray.set(bytes, 0);
|
||||
return {
|
||||
buffer : dataBuffer,
|
||||
array : dataArray,
|
||||
view : new DataView(dataBuffer)
|
||||
};
|
||||
}
|
||||
|
||||
// Readers
|
||||
function Reader() {
|
||||
}
|
||||
|
||||
function TextReader(text) {
|
||||
var that = this, blobReader;
|
||||
|
||||
function init(callback, onerror) {
|
||||
var blob = new Blob([ text ], {
|
||||
type : TEXT_PLAIN
|
||||
});
|
||||
blobReader = new BlobReader(blob);
|
||||
blobReader.init(function() {
|
||||
that.size = blobReader.size;
|
||||
callback();
|
||||
}, onerror);
|
||||
}
|
||||
|
||||
function readUint8Array(index, length, callback, onerror) {
|
||||
blobReader.readUint8Array(index, length, callback, onerror);
|
||||
}
|
||||
|
||||
that.size = 0;
|
||||
that.init = init;
|
||||
that.readUint8Array = readUint8Array;
|
||||
}
|
||||
TextReader.prototype = new Reader();
|
||||
TextReader.prototype.constructor = TextReader;
|
||||
|
||||
function Data64URIReader(dataURI) {
|
||||
var that = this, dataStart;
|
||||
|
||||
function init(callback) {
|
||||
var dataEnd = dataURI.length;
|
||||
while (dataURI.charAt(dataEnd - 1) == "=")
|
||||
dataEnd--;
|
||||
dataStart = dataURI.indexOf(",") + 1;
|
||||
that.size = Math.floor((dataEnd - dataStart) * 0.75);
|
||||
callback();
|
||||
}
|
||||
|
||||
function readUint8Array(index, length, callback) {
|
||||
var i, data = getDataHelper(length);
|
||||
var start = Math.floor(index / 3) * 4;
|
||||
var end = Math.ceil((index + length) / 3) * 4;
|
||||
var bytes = obj.atob(dataURI.substring(start + dataStart, end + dataStart));
|
||||
var delta = index - Math.floor(start / 4) * 3;
|
||||
for (i = delta; i < delta + length; i++)
|
||||
data.array[i - delta] = bytes.charCodeAt(i);
|
||||
callback(data.array);
|
||||
}
|
||||
|
||||
that.size = 0;
|
||||
that.init = init;
|
||||
that.readUint8Array = readUint8Array;
|
||||
}
|
||||
Data64URIReader.prototype = new Reader();
|
||||
Data64URIReader.prototype.constructor = Data64URIReader;
|
||||
|
||||
function BlobReader(blob) {
|
||||
var that = this;
|
||||
|
||||
function init(callback) {
|
||||
that.size = blob.size;
|
||||
callback();
|
||||
}
|
||||
|
||||
function readUint8Array(index, length, callback, onerror) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
callback(new Uint8Array(e.target.result));
|
||||
};
|
||||
reader.onerror = onerror;
|
||||
try {
|
||||
reader.readAsArrayBuffer(blobSlice(blob, index, length));
|
||||
} catch (e) {
|
||||
onerror(e);
|
||||
}
|
||||
}
|
||||
|
||||
that.size = 0;
|
||||
that.init = init;
|
||||
that.readUint8Array = readUint8Array;
|
||||
}
|
||||
BlobReader.prototype = new Reader();
|
||||
BlobReader.prototype.constructor = BlobReader;
|
||||
|
||||
// Writers
|
||||
|
||||
function Writer() {
|
||||
}
|
||||
Writer.prototype.getData = function(callback) {
|
||||
callback(this.data);
|
||||
};
|
||||
|
||||
function TextWriter(encoding) {
|
||||
var that = this, blob;
|
||||
|
||||
function init(callback) {
|
||||
blob = new Blob([], {
|
||||
type : TEXT_PLAIN
|
||||
});
|
||||
callback();
|
||||
}
|
||||
|
||||
function writeUint8Array(array, callback) {
|
||||
blob = new Blob([ blob, appendABViewSupported ? array : array.buffer ], {
|
||||
type : TEXT_PLAIN
|
||||
});
|
||||
callback();
|
||||
}
|
||||
|
||||
function getData(callback, onerror) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
callback(e.target.result);
|
||||
};
|
||||
reader.onerror = onerror;
|
||||
reader.readAsText(blob, encoding);
|
||||
}
|
||||
|
||||
that.init = init;
|
||||
that.writeUint8Array = writeUint8Array;
|
||||
that.getData = getData;
|
||||
}
|
||||
TextWriter.prototype = new Writer();
|
||||
TextWriter.prototype.constructor = TextWriter;
|
||||
|
||||
function Data64URIWriter(contentType) {
|
||||
var that = this, data = "", pending = "";
|
||||
|
||||
function init(callback) {
|
||||
data += "data:" + (contentType || "") + ";base64,";
|
||||
callback();
|
||||
}
|
||||
|
||||
function writeUint8Array(array, callback) {
|
||||
var i, delta = pending.length, dataString = pending;
|
||||
pending = "";
|
||||
for (i = 0; i < (Math.floor((delta + array.length) / 3) * 3) - delta; i++)
|
||||
dataString += String.fromCharCode(array[i]);
|
||||
for (; i < array.length; i++)
|
||||
pending += String.fromCharCode(array[i]);
|
||||
if (dataString.length > 2)
|
||||
data += obj.btoa(dataString);
|
||||
else
|
||||
pending = dataString;
|
||||
callback();
|
||||
}
|
||||
|
||||
function getData(callback) {
|
||||
callback(data + obj.btoa(pending));
|
||||
}
|
||||
|
||||
that.init = init;
|
||||
that.writeUint8Array = writeUint8Array;
|
||||
that.getData = getData;
|
||||
}
|
||||
Data64URIWriter.prototype = new Writer();
|
||||
Data64URIWriter.prototype.constructor = Data64URIWriter;
|
||||
|
||||
function BlobWriter(contentType) {
|
||||
var blob, that = this;
|
||||
|
||||
function init(callback) {
|
||||
blob = new Blob([], {
|
||||
type : contentType
|
||||
});
|
||||
callback();
|
||||
}
|
||||
|
||||
function writeUint8Array(array, callback) {
|
||||
blob = new Blob([ blob, appendABViewSupported ? array : array.buffer ], {
|
||||
type : contentType
|
||||
});
|
||||
callback();
|
||||
}
|
||||
|
||||
function getData(callback) {
|
||||
callback(blob);
|
||||
}
|
||||
|
||||
that.init = init;
|
||||
that.writeUint8Array = writeUint8Array;
|
||||
that.getData = getData;
|
||||
}
|
||||
BlobWriter.prototype = new Writer();
|
||||
BlobWriter.prototype.constructor = BlobWriter;
|
||||
|
||||
/**
|
||||
* inflate/deflate core functions
|
||||
* @param worker {Worker} web worker for the task.
|
||||
* @param initialMessage {Object} initial message to be sent to the worker. should contain
|
||||
* sn(serial number for distinguishing multiple tasks sent to the worker), and codecClass.
|
||||
* This function may add more properties before sending.
|
||||
*/
|
||||
function launchWorkerProcess(worker, initialMessage, reader, writer, offset, size, onprogress, onend, onreaderror, onwriteerror) {
|
||||
var chunkIndex = 0, index, outputSize, sn = initialMessage.sn, crc;
|
||||
|
||||
function onflush() {
|
||||
worker.removeEventListener('message', onmessage, false);
|
||||
onend(outputSize, crc);
|
||||
}
|
||||
|
||||
function onmessage(event) {
|
||||
var message = event.data, data = message.data, err = message.error;
|
||||
if (err) {
|
||||
err.toString = function () { return 'Error: ' + this.message; };
|
||||
onreaderror(err);
|
||||
return;
|
||||
}
|
||||
if (message.sn !== sn)
|
||||
return;
|
||||
if (typeof message.codecTime === 'number')
|
||||
worker.codecTime += message.codecTime; // should be before onflush()
|
||||
if (typeof message.crcTime === 'number')
|
||||
worker.crcTime += message.crcTime;
|
||||
|
||||
switch (message.type) {
|
||||
case 'append':
|
||||
if (data) {
|
||||
outputSize += data.length;
|
||||
writer.writeUint8Array(data, function() {
|
||||
step();
|
||||
}, onwriteerror);
|
||||
} else
|
||||
step();
|
||||
break;
|
||||
case 'flush':
|
||||
crc = message.crc;
|
||||
if (data) {
|
||||
outputSize += data.length;
|
||||
writer.writeUint8Array(data, function() {
|
||||
onflush();
|
||||
}, onwriteerror);
|
||||
} else
|
||||
onflush();
|
||||
break;
|
||||
case 'progress':
|
||||
if (onprogress)
|
||||
onprogress(index + message.loaded, size);
|
||||
break;
|
||||
case 'importScripts': //no need to handle here
|
||||
case 'newTask':
|
||||
case 'echo':
|
||||
break;
|
||||
default:
|
||||
console.warn('zip.js:launchWorkerProcess: unknown message: ', message);
|
||||
}
|
||||
}
|
||||
|
||||
function step() {
|
||||
index = chunkIndex * CHUNK_SIZE;
|
||||
if (index < size) {
|
||||
reader.readUint8Array(offset + index, Math.min(CHUNK_SIZE, size - index), function(array) {
|
||||
if (onprogress)
|
||||
onprogress(index, size);
|
||||
var msg = index === 0 ? initialMessage : {sn : sn};
|
||||
msg.type = 'append';
|
||||
msg.data = array;
|
||||
worker.postMessage(msg, [array.buffer]);
|
||||
chunkIndex++;
|
||||
}, onreaderror);
|
||||
} else {
|
||||
worker.postMessage({
|
||||
sn: sn,
|
||||
type: 'flush'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
outputSize = 0;
|
||||
worker.addEventListener('message', onmessage, false);
|
||||
step();
|
||||
}
|
||||
|
||||
function launchProcess(process, reader, writer, offset, size, crcType, onprogress, onend, onreaderror, onwriteerror) {
|
||||
var chunkIndex = 0, index, outputSize = 0,
|
||||
crcInput = crcType === 'input',
|
||||
crcOutput = crcType === 'output',
|
||||
crc = new Crc32();
|
||||
function step() {
|
||||
var outputData;
|
||||
index = chunkIndex * CHUNK_SIZE;
|
||||
if (index < size)
|
||||
reader.readUint8Array(offset + index, Math.min(CHUNK_SIZE, size - index), function(inputData) {
|
||||
var outputData;
|
||||
try {
|
||||
outputData = process.append(inputData, function(loaded) {
|
||||
if (onprogress)
|
||||
onprogress(index + loaded, size);
|
||||
});
|
||||
} catch (e) {
|
||||
onreaderror(e);
|
||||
return;
|
||||
}
|
||||
if (outputData) {
|
||||
outputSize += outputData.length;
|
||||
writer.writeUint8Array(outputData, function() {
|
||||
chunkIndex++;
|
||||
setTimeout(step, 1);
|
||||
}, onwriteerror);
|
||||
if (crcOutput)
|
||||
crc.append(outputData);
|
||||
} else {
|
||||
chunkIndex++;
|
||||
setTimeout(step, 1);
|
||||
}
|
||||
if (crcInput)
|
||||
crc.append(inputData);
|
||||
if (onprogress)
|
||||
onprogress(index, size);
|
||||
}, onreaderror);
|
||||
else {
|
||||
try {
|
||||
outputData = process.flush();
|
||||
} catch (e) {
|
||||
onreaderror(e);
|
||||
return;
|
||||
}
|
||||
if (outputData) {
|
||||
if (crcOutput)
|
||||
crc.append(outputData);
|
||||
outputSize += outputData.length;
|
||||
writer.writeUint8Array(outputData, function() {
|
||||
onend(outputSize, crc.get());
|
||||
}, onwriteerror);
|
||||
} else
|
||||
onend(outputSize, crc.get());
|
||||
}
|
||||
}
|
||||
|
||||
step();
|
||||
}
|
||||
|
||||
function inflate(worker, sn, reader, writer, offset, size, computeCrc32, onend, onprogress, onreaderror, onwriteerror) {
|
||||
var crcType = computeCrc32 ? 'output' : 'none';
|
||||
if (obj.zip.useWebWorkers) {
|
||||
var initialMessage = {
|
||||
sn: sn,
|
||||
codecClass: 'Inflater',
|
||||
crcType: crcType,
|
||||
};
|
||||
launchWorkerProcess(worker, initialMessage, reader, writer, offset, size, onprogress, onend, onreaderror, onwriteerror);
|
||||
} else
|
||||
launchProcess(new obj.zip.Inflater(), reader, writer, offset, size, crcType, onprogress, onend, onreaderror, onwriteerror);
|
||||
}
|
||||
|
||||
function deflate(worker, sn, reader, writer, level, onend, onprogress, onreaderror, onwriteerror) {
|
||||
var crcType = 'input';
|
||||
if (obj.zip.useWebWorkers) {
|
||||
var initialMessage = {
|
||||
sn: sn,
|
||||
options: {level: level},
|
||||
codecClass: 'Deflater',
|
||||
crcType: crcType,
|
||||
};
|
||||
launchWorkerProcess(worker, initialMessage, reader, writer, 0, reader.size, onprogress, onend, onreaderror, onwriteerror);
|
||||
} else
|
||||
launchProcess(new obj.zip.Deflater(), reader, writer, 0, reader.size, crcType, onprogress, onend, onreaderror, onwriteerror);
|
||||
}
|
||||
|
||||
function copy(worker, sn, reader, writer, offset, size, computeCrc32, onend, onprogress, onreaderror, onwriteerror) {
|
||||
var crcType = 'input';
|
||||
if (obj.zip.useWebWorkers && computeCrc32) {
|
||||
var initialMessage = {
|
||||
sn: sn,
|
||||
codecClass: 'NOOP',
|
||||
crcType: crcType,
|
||||
};
|
||||
launchWorkerProcess(worker, initialMessage, reader, writer, offset, size, onprogress, onend, onreaderror, onwriteerror);
|
||||
} else
|
||||
launchProcess(new NOOP(), reader, writer, offset, size, crcType, onprogress, onend, onreaderror, onwriteerror);
|
||||
}
|
||||
|
||||
// ZipReader
|
||||
|
||||
function decodeASCII(str) {
|
||||
var i, out = "", charCode, extendedASCII = [ '\u00C7', '\u00FC', '\u00E9', '\u00E2', '\u00E4', '\u00E0', '\u00E5', '\u00E7', '\u00EA', '\u00EB',
|
||||
'\u00E8', '\u00EF', '\u00EE', '\u00EC', '\u00C4', '\u00C5', '\u00C9', '\u00E6', '\u00C6', '\u00F4', '\u00F6', '\u00F2', '\u00FB', '\u00F9',
|
||||
'\u00FF', '\u00D6', '\u00DC', '\u00F8', '\u00A3', '\u00D8', '\u00D7', '\u0192', '\u00E1', '\u00ED', '\u00F3', '\u00FA', '\u00F1', '\u00D1',
|
||||
'\u00AA', '\u00BA', '\u00BF', '\u00AE', '\u00AC', '\u00BD', '\u00BC', '\u00A1', '\u00AB', '\u00BB', '_', '_', '_', '\u00A6', '\u00A6',
|
||||
'\u00C1', '\u00C2', '\u00C0', '\u00A9', '\u00A6', '\u00A6', '+', '+', '\u00A2', '\u00A5', '+', '+', '-', '-', '+', '-', '+', '\u00E3',
|
||||
'\u00C3', '+', '+', '-', '-', '\u00A6', '-', '+', '\u00A4', '\u00F0', '\u00D0', '\u00CA', '\u00CB', '\u00C8', 'i', '\u00CD', '\u00CE',
|
||||
'\u00CF', '+', '+', '_', '_', '\u00A6', '\u00CC', '_', '\u00D3', '\u00DF', '\u00D4', '\u00D2', '\u00F5', '\u00D5', '\u00B5', '\u00FE',
|
||||
'\u00DE', '\u00DA', '\u00DB', '\u00D9', '\u00FD', '\u00DD', '\u00AF', '\u00B4', '\u00AD', '\u00B1', '_', '\u00BE', '\u00B6', '\u00A7',
|
||||
'\u00F7', '\u00B8', '\u00B0', '\u00A8', '\u00B7', '\u00B9', '\u00B3', '\u00B2', '_', ' ' ];
|
||||
for (i = 0; i < str.length; i++) {
|
||||
charCode = str.charCodeAt(i) & 0xFF;
|
||||
if (charCode > 127)
|
||||
out += extendedASCII[charCode - 128];
|
||||
else
|
||||
out += String.fromCharCode(charCode);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function decodeUTF8(string) {
|
||||
return decodeURIComponent(escape(string));
|
||||
}
|
||||
|
||||
function getString(bytes) {
|
||||
var i, str = "";
|
||||
for (i = 0; i < bytes.length; i++)
|
||||
str += String.fromCharCode(bytes[i]);
|
||||
return str;
|
||||
}
|
||||
|
||||
function getDate(timeRaw) {
|
||||
var date = (timeRaw & 0xffff0000) >> 16, time = timeRaw & 0x0000ffff;
|
||||
try {
|
||||
return new Date(1980 + ((date & 0xFE00) >> 9), ((date & 0x01E0) >> 5) - 1, date & 0x001F, (time & 0xF800) >> 11, (time & 0x07E0) >> 5,
|
||||
(time & 0x001F) * 2, 0);
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
function readCommonHeader(entry, data, index, centralDirectory, onerror) {
|
||||
entry.version = data.view.getUint16(index, true);
|
||||
entry.bitFlag = data.view.getUint16(index + 2, true);
|
||||
entry.compressionMethod = data.view.getUint16(index + 4, true);
|
||||
entry.lastModDateRaw = data.view.getUint32(index + 6, true);
|
||||
entry.lastModDate = getDate(entry.lastModDateRaw);
|
||||
if ((entry.bitFlag & 0x01) === 0x01) {
|
||||
onerror(ERR_ENCRYPTED);
|
||||
return;
|
||||
}
|
||||
if (centralDirectory || (entry.bitFlag & 0x0008) != 0x0008) {
|
||||
entry.crc32 = data.view.getUint32(index + 10, true);
|
||||
entry.compressedSize = data.view.getUint32(index + 14, true);
|
||||
entry.uncompressedSize = data.view.getUint32(index + 18, true);
|
||||
}
|
||||
if (entry.compressedSize === 0xFFFFFFFF || entry.uncompressedSize === 0xFFFFFFFF) {
|
||||
onerror(ERR_ZIP64);
|
||||
return;
|
||||
}
|
||||
entry.filenameLength = data.view.getUint16(index + 22, true);
|
||||
entry.extraFieldLength = data.view.getUint16(index + 24, true);
|
||||
}
|
||||
|
||||
function createZipReader(reader, callback, onerror) {
|
||||
var inflateSN = 0;
|
||||
|
||||
function Entry() {
|
||||
}
|
||||
|
||||
Entry.prototype.getData = function(writer, onend, onprogress, checkCrc32) {
|
||||
var that = this;
|
||||
|
||||
function testCrc32(crc32) {
|
||||
var dataCrc32 = getDataHelper(4);
|
||||
dataCrc32.view.setUint32(0, crc32);
|
||||
return that.crc32 == dataCrc32.view.getUint32(0);
|
||||
}
|
||||
|
||||
function getWriterData(uncompressedSize, crc32) {
|
||||
if (checkCrc32 && !testCrc32(crc32))
|
||||
onerror(ERR_CRC);
|
||||
else
|
||||
writer.getData(function(data) {
|
||||
onend(data);
|
||||
});
|
||||
}
|
||||
|
||||
function onreaderror(err) {
|
||||
onerror(err || ERR_READ_DATA);
|
||||
}
|
||||
|
||||
function onwriteerror(err) {
|
||||
onerror(err || ERR_WRITE_DATA);
|
||||
}
|
||||
|
||||
reader.readUint8Array(that.offset, 30, function(bytes) {
|
||||
var data = getDataHelper(bytes.length, bytes), dataOffset;
|
||||
if (data.view.getUint32(0) != 0x504b0304) {
|
||||
onerror(ERR_BAD_FORMAT);
|
||||
return;
|
||||
}
|
||||
readCommonHeader(that, data, 4, false, onerror);
|
||||
dataOffset = that.offset + 30 + that.filenameLength + that.extraFieldLength;
|
||||
writer.init(function() {
|
||||
if (that.compressionMethod === 0)
|
||||
copy(that._worker, inflateSN++, reader, writer, dataOffset, that.compressedSize, checkCrc32, getWriterData, onprogress, onreaderror, onwriteerror);
|
||||
else
|
||||
inflate(that._worker, inflateSN++, reader, writer, dataOffset, that.compressedSize, checkCrc32, getWriterData, onprogress, onreaderror, onwriteerror);
|
||||
}, onwriteerror);
|
||||
}, onreaderror);
|
||||
};
|
||||
|
||||
function seekEOCDR(eocdrCallback) {
|
||||
// "End of central directory record" is the last part of a zip archive, and is at least 22 bytes long.
|
||||
// Zip file comment is the last part of EOCDR and has max length of 64KB,
|
||||
// so we only have to search the last 64K + 22 bytes of a archive for EOCDR signature (0x06054b50).
|
||||
var EOCDR_MIN = 22;
|
||||
if (reader.size < EOCDR_MIN) {
|
||||
onerror(ERR_BAD_FORMAT);
|
||||
return;
|
||||
}
|
||||
var ZIP_COMMENT_MAX = 256 * 256, EOCDR_MAX = EOCDR_MIN + ZIP_COMMENT_MAX;
|
||||
|
||||
// In most cases, the EOCDR is EOCDR_MIN bytes long
|
||||
doSeek(EOCDR_MIN, function() {
|
||||
// If not found, try within EOCDR_MAX bytes
|
||||
doSeek(Math.min(EOCDR_MAX, reader.size), function() {
|
||||
onerror(ERR_BAD_FORMAT);
|
||||
});
|
||||
});
|
||||
|
||||
// seek last length bytes of file for EOCDR
|
||||
function doSeek(length, eocdrNotFoundCallback) {
|
||||
reader.readUint8Array(reader.size - length, length, function(bytes) {
|
||||
for (var i = bytes.length - EOCDR_MIN; i >= 0; i--) {
|
||||
if (bytes[i] === 0x50 && bytes[i + 1] === 0x4b && bytes[i + 2] === 0x05 && bytes[i + 3] === 0x06) {
|
||||
eocdrCallback(new DataView(bytes.buffer, i, EOCDR_MIN));
|
||||
return;
|
||||
}
|
||||
}
|
||||
eocdrNotFoundCallback();
|
||||
}, function() {
|
||||
onerror(ERR_READ);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var zipReader = {
|
||||
getEntries : function(callback) {
|
||||
var worker = this._worker;
|
||||
// look for End of central directory record
|
||||
seekEOCDR(function(dataView) {
|
||||
var datalength, fileslength;
|
||||
datalength = dataView.getUint32(16, true);
|
||||
fileslength = dataView.getUint16(8, true);
|
||||
if (datalength < 0 || datalength >= reader.size) {
|
||||
onerror(ERR_BAD_FORMAT);
|
||||
return;
|
||||
}
|
||||
reader.readUint8Array(datalength, reader.size - datalength, function(bytes) {
|
||||
var i, index = 0, entries = [], entry, filename, comment, data = getDataHelper(bytes.length, bytes);
|
||||
for (i = 0; i < fileslength; i++) {
|
||||
entry = new Entry();
|
||||
entry._worker = worker;
|
||||
if (data.view.getUint32(index) != 0x504b0102) {
|
||||
onerror(ERR_BAD_FORMAT);
|
||||
return;
|
||||
}
|
||||
readCommonHeader(entry, data, index + 6, true, onerror);
|
||||
entry.commentLength = data.view.getUint16(index + 32, true);
|
||||
entry.directory = ((data.view.getUint8(index + 38) & 0x10) == 0x10);
|
||||
entry.offset = data.view.getUint32(index + 42, true);
|
||||
filename = getString(data.array.subarray(index + 46, index + 46 + entry.filenameLength));
|
||||
entry.filename = ((entry.bitFlag & 0x0800) === 0x0800) ? decodeUTF8(filename) : decodeASCII(filename);
|
||||
if (!entry.directory && entry.filename.charAt(entry.filename.length - 1) == "/")
|
||||
entry.directory = true;
|
||||
comment = getString(data.array.subarray(index + 46 + entry.filenameLength + entry.extraFieldLength, index + 46
|
||||
+ entry.filenameLength + entry.extraFieldLength + entry.commentLength));
|
||||
entry.comment = ((entry.bitFlag & 0x0800) === 0x0800) ? decodeUTF8(comment) : decodeASCII(comment);
|
||||
entries.push(entry);
|
||||
index += 46 + entry.filenameLength + entry.extraFieldLength + entry.commentLength;
|
||||
}
|
||||
callback(entries);
|
||||
}, function() {
|
||||
onerror(ERR_READ);
|
||||
});
|
||||
});
|
||||
},
|
||||
close : function(callback) {
|
||||
if (this._worker) {
|
||||
this._worker.terminate();
|
||||
this._worker = null;
|
||||
}
|
||||
if (callback)
|
||||
callback();
|
||||
},
|
||||
_worker: null
|
||||
};
|
||||
|
||||
if (!obj.zip.useWebWorkers)
|
||||
callback(zipReader);
|
||||
else {
|
||||
createWorker('inflater',
|
||||
function(worker) {
|
||||
zipReader._worker = worker;
|
||||
callback(zipReader);
|
||||
},
|
||||
function(err) {
|
||||
onerror(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ZipWriter
|
||||
|
||||
function encodeUTF8(string) {
|
||||
return unescape(encodeURIComponent(string));
|
||||
}
|
||||
|
||||
function getBytes(str) {
|
||||
var i, array = [];
|
||||
for (i = 0; i < str.length; i++)
|
||||
array.push(str.charCodeAt(i));
|
||||
return array;
|
||||
}
|
||||
|
||||
function createZipWriter(writer, callback, onerror, dontDeflate) {
|
||||
var files = {}, filenames = [], datalength = 0;
|
||||
var deflateSN = 0;
|
||||
|
||||
function onwriteerror(err) {
|
||||
onerror(err || ERR_WRITE);
|
||||
}
|
||||
|
||||
function onreaderror(err) {
|
||||
onerror(err || ERR_READ_DATA);
|
||||
}
|
||||
|
||||
var zipWriter = {
|
||||
add : function(name, reader, onend, onprogress, options) {
|
||||
var header, filename, date;
|
||||
var worker = this._worker;
|
||||
|
||||
function writeHeader(callback) {
|
||||
var data;
|
||||
date = options.lastModDate || new Date();
|
||||
header = getDataHelper(26);
|
||||
files[name] = {
|
||||
headerArray : header.array,
|
||||
directory : options.directory,
|
||||
filename : filename,
|
||||
offset : datalength,
|
||||
comment : getBytes(encodeUTF8(options.comment || ""))
|
||||
};
|
||||
header.view.setUint32(0, 0x14000808);
|
||||
if (options.version)
|
||||
header.view.setUint8(0, options.version);
|
||||
if (!dontDeflate && options.level !== 0 && !options.directory)
|
||||
header.view.setUint16(4, 0x0800);
|
||||
header.view.setUint16(6, (((date.getHours() << 6) | date.getMinutes()) << 5) | date.getSeconds() / 2, true);
|
||||
header.view.setUint16(8, ((((date.getFullYear() - 1980) << 4) | (date.getMonth() + 1)) << 5) | date.getDate(), true);
|
||||
header.view.setUint16(22, filename.length, true);
|
||||
data = getDataHelper(30 + filename.length);
|
||||
data.view.setUint32(0, 0x504b0304);
|
||||
data.array.set(header.array, 4);
|
||||
data.array.set(filename, 30);
|
||||
datalength += data.array.length;
|
||||
writer.writeUint8Array(data.array, callback, onwriteerror);
|
||||
}
|
||||
|
||||
function writeFooter(compressedLength, crc32) {
|
||||
var footer = getDataHelper(16);
|
||||
datalength += compressedLength || 0;
|
||||
footer.view.setUint32(0, 0x504b0708);
|
||||
if (typeof crc32 != "undefined") {
|
||||
header.view.setUint32(10, crc32, true);
|
||||
footer.view.setUint32(4, crc32, true);
|
||||
}
|
||||
if (reader) {
|
||||
footer.view.setUint32(8, compressedLength, true);
|
||||
header.view.setUint32(14, compressedLength, true);
|
||||
footer.view.setUint32(12, reader.size, true);
|
||||
header.view.setUint32(18, reader.size, true);
|
||||
}
|
||||
writer.writeUint8Array(footer.array, function() {
|
||||
datalength += 16;
|
||||
onend();
|
||||
}, onwriteerror);
|
||||
}
|
||||
|
||||
function writeFile() {
|
||||
options = options || {};
|
||||
name = name.trim();
|
||||
if (options.directory && name.charAt(name.length - 1) != "/")
|
||||
name += "/";
|
||||
if (files.hasOwnProperty(name)) {
|
||||
onerror(ERR_DUPLICATED_NAME);
|
||||
return;
|
||||
}
|
||||
filename = getBytes(encodeUTF8(name));
|
||||
filenames.push(name);
|
||||
writeHeader(function() {
|
||||
if (reader)
|
||||
if (dontDeflate || options.level === 0)
|
||||
copy(worker, deflateSN++, reader, writer, 0, reader.size, true, writeFooter, onprogress, onreaderror, onwriteerror);
|
||||
else
|
||||
deflate(worker, deflateSN++, reader, writer, options.level, writeFooter, onprogress, onreaderror, onwriteerror);
|
||||
else
|
||||
writeFooter();
|
||||
}, onwriteerror);
|
||||
}
|
||||
|
||||
if (reader)
|
||||
reader.init(writeFile, onreaderror);
|
||||
else
|
||||
writeFile();
|
||||
},
|
||||
close : function(callback) {
|
||||
if (this._worker) {
|
||||
this._worker.terminate();
|
||||
this._worker = null;
|
||||
}
|
||||
|
||||
var data, length = 0, index = 0, indexFilename, file;
|
||||
for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
|
||||
file = files[filenames[indexFilename]];
|
||||
length += 46 + file.filename.length + file.comment.length;
|
||||
}
|
||||
data = getDataHelper(length + 22);
|
||||
for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) {
|
||||
file = files[filenames[indexFilename]];
|
||||
data.view.setUint32(index, 0x504b0102);
|
||||
data.view.setUint16(index + 4, 0x1400);
|
||||
data.array.set(file.headerArray, index + 6);
|
||||
data.view.setUint16(index + 32, file.comment.length, true);
|
||||
if (file.directory)
|
||||
data.view.setUint8(index + 38, 0x10);
|
||||
data.view.setUint32(index + 42, file.offset, true);
|
||||
data.array.set(file.filename, index + 46);
|
||||
data.array.set(file.comment, index + 46 + file.filename.length);
|
||||
index += 46 + file.filename.length + file.comment.length;
|
||||
}
|
||||
data.view.setUint32(index, 0x504b0506);
|
||||
data.view.setUint16(index + 8, filenames.length, true);
|
||||
data.view.setUint16(index + 10, filenames.length, true);
|
||||
data.view.setUint32(index + 12, length, true);
|
||||
data.view.setUint32(index + 16, datalength, true);
|
||||
writer.writeUint8Array(data.array, function() {
|
||||
writer.getData(callback);
|
||||
}, onwriteerror);
|
||||
},
|
||||
_worker: null
|
||||
};
|
||||
|
||||
if (!obj.zip.useWebWorkers)
|
||||
callback(zipWriter);
|
||||
else {
|
||||
createWorker('deflater',
|
||||
function(worker) {
|
||||
zipWriter._worker = worker;
|
||||
callback(zipWriter);
|
||||
},
|
||||
function(err) {
|
||||
onerror(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveURLs(urls) {
|
||||
var a = document.createElement('a');
|
||||
return urls.map(function(url) {
|
||||
a.href = url;
|
||||
return a.href;
|
||||
});
|
||||
}
|
||||
|
||||
var DEFAULT_WORKER_SCRIPTS = {
|
||||
deflater: ['z-worker.js', 'deflate.js'],
|
||||
inflater: ['z-worker.js', 'inflate.js']
|
||||
};
|
||||
function createWorker(type, callback, onerror) {
|
||||
if (obj.zip.workerScripts !== null && obj.zip.workerScriptsPath !== null) {
|
||||
onerror(new Error('Either zip.workerScripts or zip.workerScriptsPath may be set, not both.'));
|
||||
return;
|
||||
}
|
||||
var scripts;
|
||||
if (obj.zip.workerScripts) {
|
||||
scripts = obj.zip.workerScripts[type];
|
||||
if (!Array.isArray(scripts)) {
|
||||
onerror(new Error('zip.workerScripts.' + type + ' is not an array!'));
|
||||
return;
|
||||
}
|
||||
scripts = resolveURLs(scripts);
|
||||
} else {
|
||||
scripts = DEFAULT_WORKER_SCRIPTS[type].slice(0);
|
||||
scripts[0] = (obj.zip.workerScriptsPath || '') + scripts[0];
|
||||
}
|
||||
var worker = new Worker(scripts[0]);
|
||||
// record total consumed time by inflater/deflater/crc32 in this worker
|
||||
worker.codecTime = worker.crcTime = 0;
|
||||
worker.postMessage({ type: 'importScripts', scripts: scripts.slice(1) });
|
||||
worker.addEventListener('message', onmessage);
|
||||
function onmessage(ev) {
|
||||
var msg = ev.data;
|
||||
if (msg.error) {
|
||||
worker.terminate(); // should before onerror(), because onerror() may throw.
|
||||
onerror(msg.error);
|
||||
return;
|
||||
}
|
||||
if (msg.type === 'importScripts') {
|
||||
worker.removeEventListener('message', onmessage);
|
||||
worker.removeEventListener('error', errorHandler);
|
||||
callback(worker);
|
||||
}
|
||||
}
|
||||
// catch entry script loading error and other unhandled errors
|
||||
worker.addEventListener('error', errorHandler);
|
||||
function errorHandler(err) {
|
||||
worker.terminate();
|
||||
onerror(err);
|
||||
}
|
||||
}
|
||||
|
||||
function onerror_default(error) {
|
||||
console.error(error);
|
||||
}
|
||||
obj.zip = {
|
||||
Reader : Reader,
|
||||
Writer : Writer,
|
||||
BlobReader : BlobReader,
|
||||
Data64URIReader : Data64URIReader,
|
||||
TextReader : TextReader,
|
||||
BlobWriter : BlobWriter,
|
||||
Data64URIWriter : Data64URIWriter,
|
||||
TextWriter : TextWriter,
|
||||
createReader : function(reader, callback, onerror) {
|
||||
onerror = onerror || onerror_default;
|
||||
|
||||
reader.init(function() {
|
||||
createZipReader(reader, callback, onerror);
|
||||
}, onerror);
|
||||
},
|
||||
createWriter : function(writer, callback, onerror, dontDeflate) {
|
||||
onerror = onerror || onerror_default;
|
||||
dontDeflate = !!dontDeflate;
|
||||
|
||||
writer.init(function() {
|
||||
createZipWriter(writer, callback, onerror, dontDeflate);
|
||||
}, onerror);
|
||||
},
|
||||
useWebWorkers : true,
|
||||
/**
|
||||
* Directory containing the default worker scripts (z-worker.js, deflate.js, and inflate.js), relative to current base url.
|
||||
* E.g.: zip.workerScripts = './';
|
||||
*/
|
||||
workerScriptsPath : null,
|
||||
/**
|
||||
* Advanced option to control which scripts are loaded in the Web worker. If this option is specified, then workerScriptsPath must not be set.
|
||||
* workerScripts.deflater/workerScripts.inflater should be arrays of urls to scripts for deflater/inflater, respectively.
|
||||
* Scripts in the array are executed in order, and the first one should be z-worker.js, which is used to start the worker.
|
||||
* All urls are relative to current base url.
|
||||
* E.g.:
|
||||
* zip.workerScripts = {
|
||||
* deflater: ['z-worker.js', 'deflate.js'],
|
||||
* inflater: ['z-worker.js', 'inflate.js']
|
||||
* };
|
||||
*/
|
||||
workerScripts : null,
|
||||
};
|
||||
|
||||
})(this);
|
||||
217
gaia/distros/spark/apps/studio/js/main.js
Executable file
217
gaia/distros/spark/apps/studio/js/main.js
Executable file
|
|
@ -0,0 +1,217 @@
|
|||
/*global
|
||||
AchievementsService,
|
||||
AutoTheme,
|
||||
Defer,
|
||||
Details,
|
||||
Navigation,
|
||||
Storage,
|
||||
ThemeCreator
|
||||
*/
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var Main = {
|
||||
panel: document.getElementById('main'),
|
||||
header: document.querySelector('#main gaia-header'),
|
||||
title: document.querySelector('#main gaia-header h1'),
|
||||
createDialog: document.getElementById('new-theme-dialog'),
|
||||
createDialogInput: document.querySelector('.new-theme-title-input'),
|
||||
createDialogConfirm: document.querySelector('#new-theme-dialog .confirm'),
|
||||
autotheme: document.querySelector('#new-theme-dialog .autotheme-palette'),
|
||||
deleteDialog: document.getElementById('delete-theme-dialog'),
|
||||
duplicateDialog: document.getElementById('duplicate-theme-dialog'),
|
||||
duplicateDialogInput: document.querySelector('.duplicate-theme-title-input'),
|
||||
duplicateDialogConfirm: document.
|
||||
querySelector('#duplicate-theme-dialog .confirm'),
|
||||
|
||||
// Create an achievements service
|
||||
achievementsService: new AchievementsService(),
|
||||
|
||||
prepareForDisplay: function(params) {
|
||||
var currentList = this.panel.querySelector('gaia-list');
|
||||
if (currentList) {
|
||||
this.panel.removeChild(currentList);
|
||||
}
|
||||
|
||||
Storage.fetchThemesList().then((themes) => {
|
||||
var list = document.createElement('gaia-list');
|
||||
themes.forEach(function(theme) {
|
||||
var link = document.createElement('a');
|
||||
link.classList.add('navigation');
|
||||
link.dataset.themeId = theme.id;
|
||||
|
||||
var title = document.createElement('h3');
|
||||
title.textContent = theme.title;
|
||||
link.appendChild(title);
|
||||
|
||||
var forward = document.createElement('i');
|
||||
forward.dataset.icon = 'forward-light';
|
||||
link.appendChild(forward);
|
||||
|
||||
list.appendChild(link);
|
||||
});
|
||||
|
||||
this.panel.appendChild(list);
|
||||
}).catch(function(error) {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
this.createDialogInput.addEventListener('input', () => {
|
||||
this.createDialogConfirm.disabled = this.createDialogInput.value === '';
|
||||
});
|
||||
|
||||
this.duplicateDialogInput.addEventListener('input', () => {
|
||||
this.duplicateDialogConfirm.disabled =
|
||||
this.duplicateDialogInput.value === '';
|
||||
});
|
||||
|
||||
return this.panel;
|
||||
},
|
||||
|
||||
promptDeleteTheme: function() {
|
||||
this.deleteDialog.open();
|
||||
this.deleteDialogDefer = new Defer();
|
||||
return this.deleteDialogDefer.promise;
|
||||
},
|
||||
|
||||
closeDeleteDialog: function() {
|
||||
this.deleteDialog.close();
|
||||
this.deleteDialogDefer = null;
|
||||
},
|
||||
|
||||
onDeleteDialogCancelClicked: function() {
|
||||
this.closeDeleteDialog();
|
||||
},
|
||||
|
||||
onDeleteDialogClicked: function() {
|
||||
this.deleteDialogDefer.resolve();
|
||||
this.closeDeleteDialog();
|
||||
},
|
||||
|
||||
promptDuplicate: function() {
|
||||
this.duplicateDialog.open();
|
||||
this.duplicateDialogDefer = new Defer();
|
||||
return this.duplicateDialogDefer.promise;
|
||||
},
|
||||
|
||||
closeDuplicateDialog: function() {
|
||||
this.duplicateDialog.close();
|
||||
this.duplicateDialogInput.value = '';
|
||||
this.duplicateDialogDefer = null;
|
||||
},
|
||||
|
||||
onDuplicateDialogCancelClicked: function() {
|
||||
this.closeDuplicateDialog();
|
||||
},
|
||||
|
||||
onDuplicateDialogClicked: function() {
|
||||
var title = this.duplicateDialogInput.value;
|
||||
this.duplicateDialogDefer.resolve(title);
|
||||
this.closeDuplicateDialog();
|
||||
},
|
||||
|
||||
createTheme: function() {
|
||||
this.promptNewTheme().then((theme) => {
|
||||
if (!theme.title) {
|
||||
throw new Error('No title has been set !');
|
||||
}
|
||||
|
||||
theme = ThemeCreator.template(theme);
|
||||
|
||||
Storage.createTheme(theme).then(() => {
|
||||
this.prepareForDisplay();
|
||||
}).then(() => this.achievementsService.reward({
|
||||
criteria: 'achievements/creative-kind',
|
||||
evidence: 'urn:studio:theme:created',
|
||||
name: 'Creative Kind',
|
||||
description: 'Use Theme Editor to create a new theme',
|
||||
image: './img/creative-kind.png'
|
||||
})).catch(function(error) {
|
||||
console.log(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
promptNewTheme() {
|
||||
window.addEventListener('AutoTheme:palette', this.onPalette);
|
||||
this.createDialog.open();
|
||||
this.createDialogDefer = new Defer();
|
||||
return this.createDialogDefer.promise;
|
||||
},
|
||||
|
||||
closeCreateDialog() {
|
||||
this.createDialog.close();
|
||||
this.createDialogInput.value = '';
|
||||
window.removeEventListener('AutoTheme:palette', this.onPalette);
|
||||
AutoTheme.clean();
|
||||
this.onPalette();
|
||||
this.createDialogDefer = null;
|
||||
},
|
||||
|
||||
onPalette() {
|
||||
// no "this" available here
|
||||
AutoTheme.showPalette(Main.autotheme);
|
||||
},
|
||||
|
||||
onCreateDialogCancelClicked() {
|
||||
this.closeCreateDialog();
|
||||
},
|
||||
|
||||
onCreateDialogCreateClicked() {
|
||||
var result = {
|
||||
title: this.createDialogInput.value,
|
||||
autotheme: AutoTheme.asStorable(),
|
||||
palette: AutoTheme.palette
|
||||
};
|
||||
this.createDialogDefer.resolve(result);
|
||||
this.closeCreateDialog();
|
||||
}
|
||||
};
|
||||
|
||||
Main.panel.addEventListener('click', function(evt) {
|
||||
var target = evt.target;
|
||||
if (target.dataset.action == 'create') {
|
||||
Main.createTheme();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target.classList.contains('navigation')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var themeId = parseInt(target.dataset.themeId);
|
||||
Navigation.push(Details.prepareForDisplay({
|
||||
id: themeId
|
||||
}));
|
||||
});
|
||||
|
||||
document.getElementById('add-theme-button').addEventListener(
|
||||
'click', () => Main.createTheme()
|
||||
);
|
||||
|
||||
Main.createDialog.querySelector('.cancel').addEventListener(
|
||||
'click', () => Main.onCreateDialogCancelClicked()
|
||||
);
|
||||
|
||||
Main.createDialog.querySelector('.confirm').addEventListener(
|
||||
'click', () => Main.onCreateDialogCreateClicked()
|
||||
);
|
||||
|
||||
Main.deleteDialog.querySelector('.cancel').addEventListener(
|
||||
'click', () => Main.onDeleteDialogCancelClicked()
|
||||
);
|
||||
|
||||
Main.deleteDialog.querySelector('.confirm').addEventListener(
|
||||
'click', () => Main.onDeleteDialogClicked()
|
||||
);
|
||||
|
||||
Main.duplicateDialog.querySelector('.cancel').addEventListener(
|
||||
'click', () => Main.onDuplicateDialogCancelClicked()
|
||||
);
|
||||
|
||||
Main.duplicateDialog.querySelector('.confirm').addEventListener(
|
||||
'click', () => Main.onDuplicateDialogClicked()
|
||||
);
|
||||
|
||||
exports.Main = Main;
|
||||
})(window);
|
||||
64
gaia/distros/spark/apps/studio/js/navigation.js
Executable file
64
gaia/distros/spark/apps/studio/js/navigation.js
Executable file
|
|
@ -0,0 +1,64 @@
|
|||
/* global
|
||||
Defer,
|
||||
Main
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var stack = [Main.prepareForDisplay()];
|
||||
|
||||
exports.Navigation = {
|
||||
waitForTransition() {
|
||||
var defer = new Defer();
|
||||
|
||||
var count = 2;
|
||||
document.body.addEventListener('transitionend', function onTransition(e) {
|
||||
if (!e.target.classList.contains('panel')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (--count) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.body.removeEventListener('transitionend', onTransition);
|
||||
defer.resolve();
|
||||
});
|
||||
|
||||
return defer.promise;
|
||||
},
|
||||
|
||||
push: function(panel) {
|
||||
return Promise.resolve(panel).then((panel) => {
|
||||
stack[stack.length - 1].classList.add('back');
|
||||
panel.classList.remove('next');
|
||||
stack.push(panel);
|
||||
|
||||
return this.waitForTransition();
|
||||
});
|
||||
},
|
||||
|
||||
pop: function() {
|
||||
var toPop = stack.pop();
|
||||
toPop.classList.add('next');
|
||||
var toDisplay = stack[stack.length - 1];
|
||||
toDisplay.classList.remove('back');
|
||||
|
||||
return this.waitForTransition().then(() => {
|
||||
toPop.dispatchEvent(new CustomEvent('Navigation:pop'));
|
||||
toDisplay.dispatchEvent(new CustomEvent('Navigation:display'));
|
||||
});
|
||||
},
|
||||
|
||||
popToRoot: function() {
|
||||
if (stack.length == 1) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.pop().then(() => {
|
||||
return this.popToRoot();
|
||||
});
|
||||
}
|
||||
};
|
||||
})(window);
|
||||
145
gaia/distros/spark/apps/studio/js/storage.js
Executable file
145
gaia/distros/spark/apps/studio/js/storage.js
Executable file
|
|
@ -0,0 +1,145 @@
|
|||
/*global
|
||||
ThemeCreator
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
exports.Storage = {
|
||||
fetchThemesList: function() {
|
||||
return getAllThemes().then(function(themes) {
|
||||
return themes.map(function(theme) {
|
||||
return {
|
||||
id: theme.id,
|
||||
title: theme.title
|
||||
};
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
fetchTheme: function(id) {
|
||||
return getTheme(id);
|
||||
},
|
||||
|
||||
createTheme: function(theme) {
|
||||
return setTheme(theme);
|
||||
},
|
||||
|
||||
forkTheme: function(theme, title) {
|
||||
delete theme.id;
|
||||
delete theme.manifestURL;
|
||||
theme.title = title;
|
||||
return setTheme(theme);
|
||||
},
|
||||
|
||||
updateTheme: function(theme) {
|
||||
return setTheme(theme);
|
||||
},
|
||||
|
||||
removeTheme: function(id) {
|
||||
return deleteTheme(id);
|
||||
}
|
||||
};
|
||||
|
||||
// IDB private stuffs
|
||||
var defaults = [
|
||||
ThemeCreator.solarized('light'),
|
||||
ThemeCreator.solarized('dark')
|
||||
];
|
||||
|
||||
var database;
|
||||
function getDB() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
if (database) {
|
||||
resolve(database);
|
||||
return;
|
||||
}
|
||||
|
||||
var req = exports.indexedDB.open('Studio', 1);
|
||||
req.onerror = function(e) {
|
||||
reject(e.target.request.errorCode);
|
||||
};
|
||||
req.onsuccess = function(e) {
|
||||
database = e.target.result;
|
||||
resolve(database);
|
||||
};
|
||||
req.onupgradeneeded = function(e) {
|
||||
var db = e.target.result;
|
||||
var store = db.createObjectStore('themes', {
|
||||
keyPath: 'id',
|
||||
autoIncrement : true
|
||||
});
|
||||
for (var i = 0; i < defaults.length; i++) {
|
||||
store.put(defaults[i]);
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getAllThemes() {
|
||||
return getDB().then(function(db) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var store = db.transaction('themes').objectStore('themes');
|
||||
|
||||
var themes = [];
|
||||
var req = store.openCursor();
|
||||
req.onsuccess = function(e) {
|
||||
var cursor = e.target.result;
|
||||
if (cursor) {
|
||||
themes.push(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
resolve(themes);
|
||||
}
|
||||
};
|
||||
req.onerror = function(e) {
|
||||
reject(e.target.errorCode);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getTheme(id) {
|
||||
return getDB().then(function(db) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var req = db.transaction('themes').objectStore('themes').get(id);
|
||||
req.onsuccess = function(e) {
|
||||
resolve(e.target.result);
|
||||
};
|
||||
req.onerror = function(e) {
|
||||
reject(e.target.errorCode);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
return getDB().then(function(db) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var store = db.transaction('themes', 'readwrite').objectStore('themes');
|
||||
var req = store.put(theme);
|
||||
req.onsuccess = function(e) {
|
||||
resolve(e.target.result);
|
||||
};
|
||||
req.onerror = function(e) {
|
||||
reject(e.target.errorCode);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function deleteTheme(id) {
|
||||
return getDB().then(function(db) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var store = db.transaction('themes', 'readwrite').objectStore('themes');
|
||||
var req = store.delete(id);
|
||||
req.onsuccess = function(e) {
|
||||
resolve(e.target.result);
|
||||
};
|
||||
req.onerror = function(e) {
|
||||
reject(e.target.errorCode);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
})(window);
|
||||
262
gaia/distros/spark/apps/studio/js/theme_creator.js
Executable file
262
gaia/distros/spark/apps/studio/js/theme_creator.js
Executable file
|
|
@ -0,0 +1,262 @@
|
|||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
const GROUPS = [
|
||||
'default', 'communications', 'media', 'productivity', 'settings'
|
||||
];
|
||||
|
||||
const SOLARIZED_LIGHT_SECTIONS = {
|
||||
'Basics': {
|
||||
'--background': '#FCF5E4',
|
||||
'--text-color': '#6A7A82',
|
||||
'--highlight-color': '#3C86CB',
|
||||
'--link-color': '#3C86CB',
|
||||
'--border-color': '#889426',
|
||||
'--button-background': '#F2F0C2',
|
||||
'--input-background': '#FCF5E4',
|
||||
'--input-color': '#6A7A82',
|
||||
'--input-clear-background': '#3C86CB',
|
||||
},
|
||||
'Header': {
|
||||
'--header-background': '#FCF5E4',
|
||||
'--header-color': '#6A7A82',
|
||||
'--header-icon-color': '#CA9630',
|
||||
'--header-button-color': '#CA9630',
|
||||
'--header-disabled-button-color': '#BDC3C3',
|
||||
'--header-action-button-color': '#CA9630'
|
||||
}
|
||||
};
|
||||
|
||||
const SOLARIZED_DARK_SECTIONS = {
|
||||
'Basics': {
|
||||
'--background': '#0E2B35',
|
||||
'--text-color': '#ffffff',
|
||||
'--highlight-color': '#889426',
|
||||
'--link-color': '#889426',
|
||||
'--border-color': '#C15082',
|
||||
'--button-background': '#3C86CB',
|
||||
'--input-background': '#0E2B35',
|
||||
'--input-color': '#6F8B94',
|
||||
'--input-clear-background': '#3C86CB',
|
||||
},
|
||||
'Header': {
|
||||
'--header-background': '#0E2B35',
|
||||
'--header-color': '#6F8B94',
|
||||
'--header-icon-color': '#C15082',
|
||||
'--header-button-color': '#C15082',
|
||||
'--header-disabled-button-color': '#BDC3C3',
|
||||
'--header-action-button-color': '#C15082'
|
||||
}
|
||||
};
|
||||
|
||||
const THEME_SOLARIZED_LIGHT = {
|
||||
title: 'Solarized Light',
|
||||
groups: {
|
||||
'default': SOLARIZED_LIGHT_SECTIONS,
|
||||
'communications': SOLARIZED_LIGHT_SECTIONS,
|
||||
'media': SOLARIZED_LIGHT_SECTIONS,
|
||||
'productivity': SOLARIZED_LIGHT_SECTIONS,
|
||||
'settings': SOLARIZED_LIGHT_SECTIONS
|
||||
}
|
||||
};
|
||||
|
||||
const THEME_SOLARIZED_DARK = {
|
||||
title: 'Solarized Dark',
|
||||
groups: {
|
||||
'default': SOLARIZED_DARK_SECTIONS,
|
||||
'communications': SOLARIZED_DARK_SECTIONS,
|
||||
'media': SOLARIZED_DARK_SECTIONS,
|
||||
'productivity': SOLARIZED_DARK_SECTIONS,
|
||||
'settings': SOLARIZED_DARK_SECTIONS
|
||||
}
|
||||
};
|
||||
|
||||
const THEME_TEMPLATE = {
|
||||
'Basics': {
|
||||
'--background': null,
|
||||
'--text-color': null,
|
||||
'--highlight-color': null,
|
||||
'--link-color': null,
|
||||
'--border-color': null,
|
||||
'--button-background': null,
|
||||
'--input-background': null,
|
||||
'--input-color': null,
|
||||
'--input-clear-background': null
|
||||
},
|
||||
'Header': {
|
||||
'--header-background': null,
|
||||
'--header-color': null,
|
||||
'--header-icon-color': null,
|
||||
'--header-button-color': null,
|
||||
'--header-disabled-button-color': null,
|
||||
'--header-action-button-color': null
|
||||
}
|
||||
};
|
||||
|
||||
const THEME_DEFAULTS = {
|
||||
default: {
|
||||
background: '#ffffff',
|
||||
textColor: '#4d4d4d',
|
||||
highlightColor: '#00caf2',
|
||||
linkColor: '#00caf2',
|
||||
borderColor: '#e7e7e7',
|
||||
buttonBackground: '#f4f4f4',
|
||||
inputBackground: '#ffffff',
|
||||
inputColor: '#333333',
|
||||
inputClearBackground: '#909ca7',
|
||||
headerBackground: '#ffffff',
|
||||
headerColor: '#4d4d4d',
|
||||
headerIconColor: '#4d4d4d',
|
||||
headerButtonColor: '#00caf2',
|
||||
headerDisabledButtonColor: '#e7e7e7',
|
||||
headerActionButtonColor: '#4d4d4d'
|
||||
},
|
||||
communications: {
|
||||
background: '#f4f4f4',
|
||||
textColor: '#27c8c2',
|
||||
highlightColor: '#00caf2',
|
||||
linkColor: '#177874',
|
||||
borderColor: '#e7e7e7',
|
||||
buttonBackground: '#f4f4f4',
|
||||
inputBackground: '#ffffff',
|
||||
inputColor: '#333333',
|
||||
inputClearBackground: '#909ca7',
|
||||
headerBackground: '#27c8c2',
|
||||
headerColor: '#ffffff',
|
||||
headerIconColor: '#ffffff',
|
||||
headerButtonColor: '#177874',
|
||||
headerDisabledButtonColor: '#e7e7e7',
|
||||
headerActionButtonColor: '#4d4d4d'
|
||||
},
|
||||
media: {
|
||||
background: '#333333',
|
||||
textColor: '#ffffff',
|
||||
highlightColor: '#00caf2',
|
||||
linkColor: '#00caf2',
|
||||
borderColor: '#4d4d4d',
|
||||
buttonBackground: '4d4d4d',
|
||||
inputBackground: '#4d4d4d',
|
||||
inputColor: '#2b2b2b',
|
||||
inputClearBackground: '#909ca7',
|
||||
headerBackground: '#333333',
|
||||
headerColor: '#ffffff',
|
||||
headerIconColor: '#ffffff',
|
||||
headerButtonColor: '#00caf2',
|
||||
headerDisabledButtonColor: '#e7e7e7',
|
||||
headerActionButtonColor: '#4d4d4d'
|
||||
},
|
||||
productivity: {
|
||||
background: '#ffffff',
|
||||
textColor: '#4d4d4d',
|
||||
highlightColor: '#27c8c2',
|
||||
linkColor: '#00caf2',
|
||||
borderColor: '#e7e7e7',
|
||||
buttonBackground: '#f4f4f4',
|
||||
inputBackground: '#ffffff',
|
||||
inputColor: '#333333',
|
||||
inputClearBackground: '#909ca7',
|
||||
headerBackground: '#ff9900',
|
||||
headerColor: '#ffffff',
|
||||
headerIconColor: '#4d4d4d',
|
||||
headerButtonColor: '#00caf2',
|
||||
headerDisabledButtonColor: '#e7e7e7',
|
||||
headerActionButtonColor: '#4d4d4d'
|
||||
},
|
||||
settings: {
|
||||
background: '#f4f4f4',
|
||||
textColor: '#4d4d4d',
|
||||
highlightColor: '#00caf2',
|
||||
linkColor: '#00caf2',
|
||||
borderColor: '#e7e7e7',
|
||||
buttonBackground: '#ffffff',
|
||||
inputBackground: '#ffffff',
|
||||
inputColor: '#333333',
|
||||
inputClearBackground: '#909ca7',
|
||||
headerBackground: '#f4f4f4',
|
||||
headerColor: '#858585',
|
||||
headerIconColor: '#4d4d4d',
|
||||
headerButtonColor: '#00caf2',
|
||||
headerDisabledButtonColor: '#e7e7e7',
|
||||
headerActionButtonColor: '#4d4d4d'
|
||||
}
|
||||
};
|
||||
|
||||
function camelVariable(variable) {
|
||||
return variable.replace(
|
||||
/-([\w-])/g,
|
||||
(_, p1) => p1 === '-' ? '' : p1.toUpperCase()
|
||||
);
|
||||
}
|
||||
|
||||
function getPaletteWithKeywords(arrayPalette) {
|
||||
if (!arrayPalette || !arrayPalette.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
var l = arrayPalette.length;
|
||||
var palette = Object.keys(THEME_DEFAULTS.default).reduce(
|
||||
(acc, keyword, i) => {
|
||||
acc[keyword] = arrayPalette[i % l];
|
||||
return acc;
|
||||
}, {}
|
||||
);
|
||||
|
||||
return palette;
|
||||
}
|
||||
|
||||
function replaceThemeTemplate(group, palette, defaultPalette) {
|
||||
var result = {};
|
||||
|
||||
Object.keys(group).forEach(variable => {
|
||||
var keyword = camelVariable(variable);
|
||||
|
||||
var color = palette[keyword];
|
||||
color = color ? color.toHexString() : defaultPalette[keyword];
|
||||
|
||||
result[variable] = color;
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
exports.ThemeCreator = {
|
||||
/**
|
||||
* @param theme { title, [autotheme], [palette] }
|
||||
* @return {
|
||||
* title, [autotheme],
|
||||
* groups { default, communications, media, productivity, settings }
|
||||
* }
|
||||
*/
|
||||
template(theme) {
|
||||
var result = Object.assign({}, theme);
|
||||
result.groups = {};
|
||||
|
||||
var paletteWithKeywords = getPaletteWithKeywords(theme.palette);
|
||||
GROUPS.forEach(group => {
|
||||
var groupValues = {};
|
||||
for (var key in THEME_TEMPLATE) {
|
||||
groupValues[key] = replaceThemeTemplate(
|
||||
THEME_TEMPLATE[key],
|
||||
paletteWithKeywords,
|
||||
THEME_DEFAULTS[group]
|
||||
);
|
||||
}
|
||||
result.groups[group] = groupValues;
|
||||
});
|
||||
|
||||
delete result.palette;
|
||||
return result;
|
||||
},
|
||||
|
||||
solarized(flavor) {
|
||||
switch(flavor) {
|
||||
case 'light':
|
||||
return THEME_SOLARIZED_LIGHT;
|
||||
case 'dark':
|
||||
return THEME_SOLARIZED_DARK;
|
||||
default:
|
||||
throw new Error('Flavor ' + flavor + ' is invalid.');
|
||||
}
|
||||
}
|
||||
};
|
||||
})(window);
|
||||
Loading…
Reference in a new issue