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.
33
gaia/shared/elements/gaia-site-icon/.bower.json
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"name": "gaia-site-icon",
|
||||
"description": "A web component to show an icon for an application or site.",
|
||||
"authors": [
|
||||
"Chris Lord <chrislord.net@gmail.com>"
|
||||
],
|
||||
"main": "script.js",
|
||||
"keywords": [
|
||||
"app",
|
||||
"icon",
|
||||
"homescreen",
|
||||
"B2G",
|
||||
"Firefox OS"
|
||||
],
|
||||
"license": "MIT",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests"
|
||||
],
|
||||
"homepage": "https://github.com/fxos-components/gaia-site-icon",
|
||||
"_release": "dd873b7f7d",
|
||||
"_resolution": {
|
||||
"type": "branch",
|
||||
"branch": "master",
|
||||
"commit": "dd873b7f7d0d8be737566276f3188e68931a9584"
|
||||
},
|
||||
"_source": "git://github.com/fxos-components/gaia-site-icon.git",
|
||||
"_target": "*",
|
||||
"_originalSource": "fxos-components/gaia-site-icon"
|
||||
}
|
||||
24
gaia/shared/elements/gaia-site-icon/bower.json
Executable file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "gaia-site-icon",
|
||||
"version": "0.0.0",
|
||||
"description": "A web component to show an icon for an application or site.",
|
||||
"authors": [
|
||||
"Chris Lord <chrislord.net@gmail.com>"
|
||||
],
|
||||
"main": "script.js",
|
||||
"keywords": [
|
||||
"app",
|
||||
"icon",
|
||||
"homescreen",
|
||||
"B2G",
|
||||
"Firefox OS"
|
||||
],
|
||||
"license": "MIT",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests"
|
||||
]
|
||||
}
|
||||
31
gaia/shared/elements/gaia-site-icon/examples/basic.html
Executable file
|
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GaiaAppIcon demo</title>
|
||||
|
||||
<script src="../script.js"></script>
|
||||
|
||||
<style>
|
||||
gaia-app-icon {
|
||||
width: 256px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<gaia-app-icon id="icon"></gaia-app-icon>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
// TODO: This example doesn't work anymore, icon takes an app and uses
|
||||
// app.manifest, app.launch and navigator.mozApps.mgmt.getIcon.
|
||||
var icon = document.getElementById('icon');
|
||||
icon.manifest = {
|
||||
name: 'GaiaAppIcon test',
|
||||
origin: '',
|
||||
icons: {
|
||||
'284': 'icon.png'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</html>
|
||||
BIN
gaia/shared/elements/gaia-site-icon/examples/icon.png
Executable file
|
After Width: | Height: | Size: 5.6 KiB |
7
gaia/shared/elements/gaia-site-icon/external/style.css
vendored
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
/* XXX Bug 1018269
|
||||
* animation keyframes have to be specified outside of the shadow DOM.
|
||||
*/
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(1deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
1
gaia/shared/elements/gaia-site-icon/images/app_install_canceled.svg
Executable file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><path opacity=".85" fill="#5F5F5F" d="M42 0c23.3 0 42 18.7 42 42S65.3 84 42 84 0 65.3 0 42 18.7 0 42 0z"/><path fill="#FFF" d="M60.7 40.1c-.9-9.3-9.3-16.8-18.7-16.8-10.3 0-18.7 8.4-18.7 18.7S31.7 60.7 42 60.7c3.7 0 7.5-.9 10.3-2.8l-2.8-3.7C46.7 56.1 44.8 57 42 57c-8.4 0-14.9-6.5-14.9-14.9s6.5-15 14.9-15c7.5 0 13.1 5.6 14 13.1h-6.5l7.5 7.5c.9.9 2.8.9 3.7 0l7.5-7.5h-7.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 443 B |
1
gaia/shared/elements/gaia-site-icon/images/app_install_failed.svg
Executable file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><path opacity=".85" fill="#E51E1E" d="M42 0c23.3 0 42 18.7 42 42S65.3 84 42 84 0 65.3 0 42 18.7 0 42 0z"/><path fill="#FFF" d="M60.7 40.1c-.9-9.3-9.3-16.8-18.7-16.8-10.3 0-18.7 8.4-18.7 18.7S31.7 60.7 42 60.7c3.7 0 7.5-.9 10.3-2.8l-2.8-3.7C46.7 56.1 44.8 57 42 57c-8.4 0-14.9-6.5-14.9-14.9s6.5-15 14.9-15c7.5 0 13.1 5.6 14 13.1h-6.5l7.5 7.5c.9.9 2.8.9 3.7 0l7.5-7.5h-7.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 443 B |
1
gaia/shared/elements/gaia-site-icon/images/app_install_spinner.svg
Executable file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="24.134" y1="31.414" x2="81.176" y2="46.699"><stop offset="0" stop-color="#00B4CF"/><stop offset=".979" stop-color="#00B4CF" stop-opacity="0"/></linearGradient><path fill-rule="evenodd" clip-rule="evenodd" fill="url(#a)" d="M83.9 39.5v2.6h-3.3c0 21.3-17.3 38.6-38.7 38.6C20.6 80.7 3.3 63.4 3.3 42 3.3 20.6 20.6 3.3 42 3.3V0C18.8 0 0 18.8 0 42s18.8 42 42 42 42-18.8 42-42c0-.8 0-1.7-.1-2.5z"/></svg>
|
||||
|
After Width: | Height: | Size: 518 B |
1
gaia/shared/elements/gaia-site-icon/images/app_install_unrecoverable.svg
Executable file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><path opacity=".85" fill="#E51E1E" d="M42 0c23.3 0 42 18.8 42 42 0 23.3-18.8 42-42 42-23.1.1-42-18.7-42-42C0 18.8 18.9 0 42 0z"/><path fill="#FFF" d="M42 25.4c-.9 0-2.5 2.5-3.4 5.5 1.1-.2 2.3-.3 3.5-.3s2.3.1 3.3.3c-.9-3-2.5-5.5-3.4-5.5M27.1 39.1l5.7-5.7c1.7-8.3 6.7-16.7 9.2-16.7s7.5 8.4 9.2 16.7l5.7 5.7V54h-4l-5.6-5.6c-.6.6-1.2 1.1-1.9 1.5h-7c-.7-.4-1.3-.9-1.9-1.5L31.1 54h-4V39.1M16.6 59.2c.4.9 1.3 1.5 2.4 1.5h46c1 0 1.9-.6 2.4-1.5H16.6z"/></svg>
|
||||
|
After Width: | Height: | Size: 513 B |
1
gaia/shared/elements/gaia-site-icon/images/app_installing.svg
Executable file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><path opacity=".85" fill="#333" d="M42 0c23.3 0 42 18.8 42 42 0 23.3-18.8 42-42 42-23.1.1-42-18.7-42-42C0 18.8 18.9 0 42 0z"/><path fill="#FFF" d="M29.9 50c.4.9 1.3 1.5 2.4 1.5h19.3c1 0 1.9-.6 2.4-1.5H29.9zm22.3-17.8L51 33.4v.1L42.4 42c-.3.3-.7.3-1 0l-8.6-8.6v-.1l-1.2-1.2c-1.6.3-3.1.6-4.6.9.2.5.4 1 .9 1.5l11.3 11.3c1.5 1.5 3.8 1.5 5.3 0l11.3-11.3c.4-.4.7-.9.9-1.5-1.4-.3-2.9-.6-4.5-.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 459 B |
1
gaia/shared/elements/gaia-site-icon/images/default_icon.svg
Executable file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="84" height="84"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="42" x2="42" y2="84"><stop offset="0" stop-color="#E5EBEF"/><stop offset="1" stop-color="#EDF1F4"/></linearGradient><path fill="url(#a)" d="M42 0c23.3 0 42 18.8 42 42 0 23.3-18.8 42-42 42-23.1.1-42-18.7-42-42C0 18.8 18.9 0 42 0z"/><path fill="#9AB0C0" d="M41.5 25.9c-.9 0-2.5 2.6-3.5 5.7 1.1-.2 2.3-.3 3.6-.3 1.2 0 2.4.1 3.4.3-1-3.1-2.6-5.7-3.5-5.7M43 53.3h2.2c.3.9.4 2 .4 3.1 0 4-3.1 7.2-4.1 7.2s-4.1-3.2-4.1-7.2c0-1.1.1-2.2.4-3.1H40c-.2.5-.3 1.2-.3 1.9 0 1.8 1.4 3.3 1.9 3.3s1.9-1.5 1.9-3.3c-.2-.7-.3-1.4-.5-1.9M26.1 40l5.9-5.9c1.7-8.6 6.9-17.2 9.5-17.2s7.8 8.7 9.5 17.2l5.9 5.9v15.4h-4.1l-5.7-5.7c-.6.6-1.2 1.1-1.9 1.5h-7.3c-.7-.4-1.3-.9-1.9-1.5l-5.7 5.7h-4.1L26.1 40"/></svg>
|
||||
|
After Width: | Height: | Size: 799 B |
39
gaia/shared/elements/gaia-site-icon/package.json
Executable file
|
|
@ -0,0 +1,39 @@
|
|||
{
|
||||
"name": "gaia-site-icon",
|
||||
"version": "0.0.1",
|
||||
"description": "A web component to show an icon for an application or site.",
|
||||
"author": [
|
||||
{
|
||||
"name": "Chris Lord",
|
||||
"email": "chrislord.net@gmail.com"
|
||||
}
|
||||
],
|
||||
"main": "script.js",
|
||||
"directories": {
|
||||
"example": "examples"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "./node_modules/karma/bin/karma start test/karma.conf.js --single-run",
|
||||
"test-dev": "./node_modules/karma/bin/karma start test/karma.conf.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:gaia-components/gaia-site-icon.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"karma": "^0.12.37",
|
||||
"karma-firefox-launcher": "^0.1.6",
|
||||
"karma-mocha": "^0.2.0",
|
||||
"karma-sinon-chai": "^1.0.0",
|
||||
"mocha": "^2.2.5",
|
||||
"mozilla-download": "^1.1.1"
|
||||
},
|
||||
"keywords": [
|
||||
"app",
|
||||
"icon",
|
||||
"homescreen",
|
||||
"B2G",
|
||||
"Firefox OS"
|
||||
],
|
||||
"license": "MIT"
|
||||
}
|
||||
607
gaia/shared/elements/gaia-site-icon/script.js
Executable file
|
|
@ -0,0 +1,607 @@
|
|||
'use strict';
|
||||
|
||||
window.GaiaAppIcon = (function(exports) {
|
||||
const LAUNCH_TIMEOUT = 500;
|
||||
|
||||
const PREDEFINED_ICONS = {
|
||||
default: 'default_icon.svg',
|
||||
unrecoverable: 'app_install_unrecoverable.svg',
|
||||
installing: 'app_installing.svg',
|
||||
paused: 'app_install_canceled.svg',
|
||||
failed: 'app_install_failed.svg'
|
||||
};
|
||||
|
||||
const SHADOW_BLUR = 1;
|
||||
const SHADOW_OFFSET = { x: 1, y: 1 };
|
||||
const SHADOW_COLOR = 'rgba(0, 0, 0, 0.2)';
|
||||
const DEFAULT_BACKGROUND_COLOR = 'rgb(228, 234, 238)';
|
||||
const CANVAS_PADDING = 2 * window.devicePixelRatio;
|
||||
const FAVICON_SCALE = 0.55;
|
||||
|
||||
var proto = Object.create(HTMLElement.prototype);
|
||||
|
||||
// Allow the base URL to be overridden
|
||||
var base = window.GAIA_ICONS_BASE_URL ||
|
||||
window.COMPONENTS_BASE_URL ||
|
||||
'bower_components/';
|
||||
|
||||
var baseurl = window.GaiaAppIconBaseurl || base + 'gaia-site-icon/';
|
||||
|
||||
proto.createdCallback = function() {
|
||||
this._template = template.content.cloneNode(true);
|
||||
|
||||
var shadow = this.createShadowRoot();
|
||||
shadow.appendChild(this._template);
|
||||
|
||||
this._container = shadow.getElementById('image-container');
|
||||
this._titleContainer = shadow.getElementById('title-container');
|
||||
this._subtitle = shadow.getElementById('subtitle');
|
||||
this._showName = true;
|
||||
this._image = null;
|
||||
this._app = null;
|
||||
this._bookmark = null;
|
||||
this._entryPoint = '';
|
||||
this._size = -1;
|
||||
this._hasIcon = false;
|
||||
this._hasUserSetIcon = false;
|
||||
this._hasPredefinedIcon = false;
|
||||
this._iconUrl = null;
|
||||
this._pendingIconUrl = null;
|
||||
this._pendingIconRefresh = false;
|
||||
this._lastState = null;
|
||||
|
||||
this._predefinedIcons = {};
|
||||
for (var key in PREDEFINED_ICONS) {
|
||||
this._predefinedIcons[key] = baseurl + 'images/' + PREDEFINED_ICONS[key];
|
||||
}
|
||||
|
||||
// Make the icon accessible/allow activation
|
||||
this.tabIndex = 0;
|
||||
this.setAttribute('role', 'link');
|
||||
|
||||
var activate = () => {
|
||||
if (!this.dispatchEvent(new CustomEvent('activated',
|
||||
{ cancelable: true }))) {
|
||||
return;
|
||||
}
|
||||
this.launch();
|
||||
};
|
||||
|
||||
this.addEventListener('keydown', (e) => {
|
||||
switch (e.keyCode) {
|
||||
case 32: // Space
|
||||
case 13: // Enter
|
||||
activate();
|
||||
}
|
||||
});
|
||||
this.addEventListener('click', activate);
|
||||
|
||||
// Refresh now we've created the DOM
|
||||
this.refresh();
|
||||
};
|
||||
|
||||
Object.defineProperty(proto, 'app', {
|
||||
get: function() {
|
||||
return this._app;
|
||||
},
|
||||
|
||||
set: function(app) {
|
||||
if (this._app) {
|
||||
this._app.removeEventListener('progress', this);
|
||||
this._app.removeEventListener('downloaderror', this);
|
||||
this._app.removeEventListener('downloadsuccess', this);
|
||||
this._app.removeEventListener('downloadapplied', this);
|
||||
}
|
||||
|
||||
delete this.dataset.testIconUrl;
|
||||
this._cancelIconLoad();
|
||||
this._removeOldIcon();
|
||||
this._hasIcon = false;
|
||||
|
||||
this._app = app;
|
||||
if (!app) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._bookmark = null;
|
||||
this._app.addEventListener('progress', this);
|
||||
this._app.addEventListener('downloaderror', this);
|
||||
this._app.addEventListener('downloadsuccess', this);
|
||||
this._app.addEventListener('downloadapplied', this);
|
||||
|
||||
this._container.classList.add('initial-load');
|
||||
},
|
||||
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'entryPoint', {
|
||||
get: function() {
|
||||
return this._entryPoint;
|
||||
},
|
||||
|
||||
set: function(entryPoint) {
|
||||
this._entryPoint = entryPoint ? entryPoint : '';
|
||||
this.refresh();
|
||||
},
|
||||
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'state', {
|
||||
get: function() {
|
||||
if (!this.app) {
|
||||
return 'unavailable';
|
||||
}
|
||||
|
||||
if (this.app.installState === 'pending' &&
|
||||
!this.app.downloadAvailable &&
|
||||
!this.app.readyToApplyDownload) {
|
||||
return 'unrecoverable';
|
||||
}
|
||||
|
||||
if (this.app.downloading) {
|
||||
return 'installing';
|
||||
}
|
||||
|
||||
if (this.app.downloadError) {
|
||||
if (this.app.downloadError.name === 'DOWNLOAD_CANCELED') {
|
||||
return 'paused';
|
||||
} else {
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.app.installState === 'pending') {
|
||||
return 'paused';
|
||||
}
|
||||
|
||||
return 'installed';
|
||||
},
|
||||
|
||||
enumberable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'name', {
|
||||
get: function() {
|
||||
return this._subtitle ? this._subtitle.textContent : '';
|
||||
},
|
||||
|
||||
enumberable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'size', {
|
||||
get: function() {
|
||||
return (this._size === -1) ? this.clientWidth : this._size;
|
||||
},
|
||||
|
||||
set: function(size) {
|
||||
this._size = size >= 0 ? size : -1;
|
||||
},
|
||||
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'bookmark', {
|
||||
get: function() {
|
||||
return this._bookmark;
|
||||
},
|
||||
|
||||
set: function(bookmark) {
|
||||
if (!this._bookmark || this._bookmark.id !== bookmark.id) {
|
||||
this.app = null;
|
||||
}
|
||||
this._bookmark = bookmark;
|
||||
},
|
||||
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'icon', {
|
||||
get: function() {
|
||||
if (window.VulpesCompat && this.app && this._vulpesSourceIcon) return Promise.resolve(this._vulpesSourceIcon);
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this._hasIcon || this._hasPredefinedIcon) {
|
||||
return reject();
|
||||
}
|
||||
|
||||
var image = this._container.querySelector('img');
|
||||
if (!image) {
|
||||
return reject();
|
||||
}
|
||||
|
||||
var canvas = document.createElement('canvas');
|
||||
var size = this.size * window.devicePixelRatio;
|
||||
canvas.width = canvas.height = size;
|
||||
var ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
ctx.drawImage(image, 0, 0, size, size);
|
||||
try {
|
||||
canvas.toBlob((blob) => {
|
||||
resolve(blob);
|
||||
});
|
||||
} catch(e) {
|
||||
console.error('Failed to get canvas data for icon');
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
set: function(blob) {
|
||||
if (blob) {
|
||||
this._prepareIconLoader();
|
||||
this._hasUserSetIcon = true;
|
||||
this._pendingIconUrl = 'user-set';
|
||||
this._image.src = URL.createObjectURL(blob);
|
||||
} else {
|
||||
this._pendingIconUrl = null;
|
||||
delete this.dataset.testIconUrl;
|
||||
this._cancelIconLoad();
|
||||
this._removeOldIcon();
|
||||
}
|
||||
},
|
||||
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'showName', {
|
||||
get: function() {
|
||||
return this._showName;
|
||||
},
|
||||
|
||||
set: function(show) {
|
||||
show = show ? true : false;
|
||||
if (this._showName !== show) {
|
||||
this._showName = show;
|
||||
this._titleContainer.classList.toggle('hidden', !show);
|
||||
}
|
||||
},
|
||||
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(proto, 'isUserSet', {
|
||||
get: function() {
|
||||
return this._iconUrl === 'user-set';
|
||||
},
|
||||
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
proto._cancelIconLoad = function() {
|
||||
if (!this._image) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._pendingIconUrl = null;
|
||||
this._image.onload = null;
|
||||
this._image.src = '';
|
||||
this._image = null;
|
||||
};
|
||||
|
||||
proto._removeOldIcon = function() {
|
||||
while (this._container.children.length > 1) {
|
||||
this._container.removeChild(this._container.lastChild);
|
||||
}
|
||||
};
|
||||
|
||||
proto.launch = function() {
|
||||
if (!this.app && !this.bookmark) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.classList.add('launching');
|
||||
setTimeout(() => { this.classList.remove('launching'); }, LAUNCH_TIMEOUT);
|
||||
|
||||
if (this.app) {
|
||||
window.performance.mark('appLaunch@' + this.app.origin);
|
||||
this.app.launch(this.entryPoint);
|
||||
} else {
|
||||
var features = {
|
||||
name: this.bookmark.name,
|
||||
remote: true
|
||||
};
|
||||
|
||||
if (this.bookmark.scope) {
|
||||
features.scope = this.bookmark.scope;
|
||||
}
|
||||
|
||||
window.open(this.bookmark.url, '_samescope', Object.keys(features)
|
||||
.map(function eachFeature(key) {
|
||||
return encodeURIComponent(key) + '=' +
|
||||
encodeURIComponent(features[key]);
|
||||
}).join(','));
|
||||
}
|
||||
};
|
||||
|
||||
proto._prepareIconLoader = function() {
|
||||
// Create new image element to load icon
|
||||
this._cancelIconLoad();
|
||||
this._hasPredefinedIcon = false;
|
||||
this._hasUserSetIcon = false;
|
||||
this._image = document.createElement('img');
|
||||
this._image.setAttribute('role', 'presentation');
|
||||
|
||||
this._image.onload = () => {
|
||||
this._image.onload = () => {
|
||||
// Add new icon
|
||||
this._removeOldIcon();
|
||||
this._container.appendChild(this._image);
|
||||
this._container.classList.remove('initial-load');
|
||||
this._image = null;
|
||||
this._hasIcon = true;
|
||||
|
||||
// Set icon URL on dataset, for testing.
|
||||
if (this._pendingIconUrl) {
|
||||
this._iconUrl = this.dataset.testIconUrl = this._pendingIconUrl;
|
||||
this._pendingIconUrl = null;
|
||||
}
|
||||
|
||||
if (!this._hasPredefinedIcon) {
|
||||
this.dispatchEvent(new CustomEvent('icon-loaded'));
|
||||
}
|
||||
|
||||
if (this._pendingIconRefresh) {
|
||||
this._image = null;
|
||||
this._pendingIconRefresh = false;
|
||||
this.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
if (!this._hasUserSetIcon) {
|
||||
// Process the image
|
||||
var canvas = document.createElement('canvas');
|
||||
var size = this.size * window.devicePixelRatio;
|
||||
canvas.width = canvas.height = size;
|
||||
|
||||
var ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
ctx.shadowColor = SHADOW_COLOR;
|
||||
ctx.shadowBlur = SHADOW_BLUR;
|
||||
ctx.shadowOffsetX = SHADOW_OFFSET.x;
|
||||
ctx.shadowOffsetY = SHADOW_OFFSET.y;
|
||||
|
||||
if (this.bookmark && !this._hasPredefinedIcon) {
|
||||
if (this._image.width <= size / 2) {
|
||||
// Draw filled background circle
|
||||
ctx.beginPath();
|
||||
ctx.arc(size / 2, size / 2,
|
||||
size / 2 - CANVAS_PADDING, 0, 2 * Math.PI);
|
||||
ctx.fillStyle = DEFAULT_BACKGROUND_COLOR;
|
||||
ctx.fill();
|
||||
|
||||
// Disable shadow drawing and image smoothing
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.shadowOffsetY = 0;
|
||||
ctx.mozImageSmoothingEnabled = false;
|
||||
|
||||
var iconSize = size * FAVICON_SCALE;
|
||||
ctx.drawImage(this._image,
|
||||
(size - iconSize) / 2,
|
||||
(size - iconSize) / 2,
|
||||
iconSize, iconSize);
|
||||
} else {
|
||||
// Clip the favicon in a circle. We do this in a new canvas so
|
||||
// the shadow-rendering on the main canvas works correctly.
|
||||
var clipCanvas = document.createElement('canvas');
|
||||
clipCanvas.width = clipCanvas.height = size;
|
||||
|
||||
var clipCtx =
|
||||
clipCanvas.getContext('2d', { willReadFrequently: true });
|
||||
clipCtx.beginPath();
|
||||
clipCtx.arc(size / 2, size / 2,
|
||||
size / 2 - CANVAS_PADDING, 0, 2 * Math.PI);
|
||||
clipCtx.clip();
|
||||
|
||||
clipCtx.drawImage(this._image,
|
||||
CANVAS_PADDING, CANVAS_PADDING,
|
||||
size - CANVAS_PADDING * 2,
|
||||
size - CANVAS_PADDING * 2);
|
||||
ctx.drawImage(clipCanvas, 0, 0);
|
||||
}
|
||||
} else {
|
||||
// Just draw the icon straight (without padding if it was set
|
||||
// explicitly rather than loaded via manifest/bookmark)
|
||||
ctx.drawImage(this._image,
|
||||
CANVAS_PADDING, CANVAS_PADDING,
|
||||
size - CANVAS_PADDING * 2,
|
||||
size - CANVAS_PADDING * 2);
|
||||
}
|
||||
|
||||
canvas.toBlob(function(image, blob) {
|
||||
if (image.onload) {
|
||||
image.src = URL.createObjectURL(blob);
|
||||
}
|
||||
}.bind(this, this._image));
|
||||
}
|
||||
|
||||
if (this._hasUserSetIcon) {
|
||||
this._image.onload();
|
||||
}
|
||||
};
|
||||
|
||||
this._image.onerror = (e) => {
|
||||
console.error('Failed to load icon', e);
|
||||
this._pendingIconUrl = null;
|
||||
|
||||
if (this._pendingIconRefresh) {
|
||||
this._pendingIconRefresh = false;
|
||||
this._image = null;
|
||||
this.refresh();
|
||||
} else if (!this._hasIcon) {
|
||||
this._image.onerror = null;
|
||||
this._setPredefinedIcon('default');
|
||||
} else {
|
||||
this._image = null;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
proto._setPredefinedIcon = function(name) {
|
||||
this._hasPredefinedIcon = true;
|
||||
this._image.src = this._pendingIconUrl = this._predefinedIcons[name];
|
||||
};
|
||||
|
||||
proto._localizeString = function(str) {
|
||||
var userLang = document.documentElement.lang;
|
||||
|
||||
// We want to make sure that we tranlsate only if we're using
|
||||
// a runtime pseudolocale.
|
||||
// mozL10n.ctx.qps contains only runtime pseudolocales
|
||||
if (navigator.mozL10n &&
|
||||
navigator.mozL10n.ctx.qps.indexOf(userLang) !== -1) {
|
||||
return navigator.mozL10n.qps[userLang].translate(str);
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
proto.updateName = function() {
|
||||
if (this.app) {
|
||||
var userLang = document.documentElement.lang;
|
||||
var ep = this.entryPoint || undefined;
|
||||
|
||||
this.app.getLocalizedValue('short_name', userLang, ep).then(
|
||||
(shortName) => {
|
||||
this._subtitle.textContent = this._localizeString(shortName);
|
||||
},
|
||||
() => {
|
||||
this.app.getLocalizedValue('name', userLang, ep).then(
|
||||
(name) => {
|
||||
this._subtitle.textContent = this._localizeString(name);
|
||||
},
|
||||
(e) => {
|
||||
// Try to fall back to manifest app name
|
||||
var manifest = this.app.manifest || this.app.updateManifest;
|
||||
if (manifest) {
|
||||
var nameObject = (manifest.entry_points && this.entryPoint) ?
|
||||
manifest.entry_points[this.entryPoint] : manifest;
|
||||
this._subtitle.textContent =
|
||||
this._localizeString(nameObject.short_name || nameObject.name);
|
||||
} else {
|
||||
console.error('Error retrieving app name', e);
|
||||
this._subtitle.textContent = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
} else if (this.bookmark) {
|
||||
this._subtitle.textContent = this.bookmark.name;
|
||||
} else {
|
||||
this._subtitle.textContent = '';
|
||||
}
|
||||
};
|
||||
|
||||
proto.refresh = function() {
|
||||
if (!this._template) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.updateName();
|
||||
this._container.classList.remove('downloading');
|
||||
|
||||
// Set an identifier on the icon to help with automated testing.
|
||||
if (this.app) {
|
||||
this.dataset.identifier = this.app.manifestURL +
|
||||
(this.entryPoint ? '/' + this.entryPoint : '');
|
||||
} else if (this.bookmark) {
|
||||
this.dataset.identifier = this.bookmark.id;
|
||||
} else {
|
||||
delete this.dataset.identifier;
|
||||
}
|
||||
|
||||
// If we're already loading an icon, let that finish first.
|
||||
if (this._image) {
|
||||
this._pendingIconRefresh = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// If the icon won't be visible, don't bother fetching and processing it.
|
||||
var size = this.size * window.devicePixelRatio;
|
||||
if (size < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._prepareIconLoader();
|
||||
|
||||
// Handle icon loading for bookmarks, app icon loading is more involved
|
||||
// and handled below this block.
|
||||
if (this.bookmark) {
|
||||
// Fallback to the default icon
|
||||
if (!this._hasIcon) {
|
||||
this._setPredefinedIcon('default');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If the app is downloading/paused/failed to download, use a predefined
|
||||
// icon.
|
||||
var state = this._lastState = this.state;
|
||||
switch (state) {
|
||||
case 'unrecoverable':
|
||||
this._setPredefinedIcon('unrecoverable');
|
||||
break;
|
||||
|
||||
case 'installing':
|
||||
this._setPredefinedIcon('installing');
|
||||
this._container.classList.add('downloading');
|
||||
break;
|
||||
|
||||
case 'paused':
|
||||
this._setPredefinedIcon('paused');
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
this._setPredefinedIcon('failed');
|
||||
break;
|
||||
|
||||
case 'installed':
|
||||
var handleError = function(image, e) {
|
||||
if (e.message !== 'NO_ICON' && e.name !== 'NO_ICON') {
|
||||
console.error('Failed to retrieve icon', e);
|
||||
}
|
||||
if (image.onload && !this._hasIcon) {
|
||||
this._setPredefinedIcon('default');
|
||||
} else {
|
||||
this._image = null;
|
||||
}
|
||||
};
|
||||
|
||||
var getImage = () => {
|
||||
navigator.mozApps.mgmt.getIcon(this.app, size, this.entryPoint).
|
||||
then(function(image, blob) {
|
||||
this._pendingIconUrl = 'app-icon';
|
||||
if (window.VulpesCompat) { this._vulpesSourceIcon = blob; this._hasUserSetIcon = true; } // Keep original app pixels; skip the legacy canvas pass.
|
||||
if (image.onload) {
|
||||
image.src = URL.createObjectURL(blob);
|
||||
}
|
||||
}.bind(this, this._image),
|
||||
handleError.bind(this, this._image));
|
||||
};
|
||||
|
||||
getImage();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
proto.handleEvent = function(e) {
|
||||
switch(e.type) {
|
||||
case 'progress':
|
||||
case 'downloadsuccess':
|
||||
if (this.state !== this._lastState) {
|
||||
this.refresh();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'downloaderror':
|
||||
case 'downloadapplied':
|
||||
this.refresh();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
var template = document.createElement('template');
|
||||
var stylesheet = baseurl + 'style.css';
|
||||
template.innerHTML =
|
||||
`<style>@import url(${stylesheet});</style>
|
||||
<div id="image-container"><div id="spinner"></div></div>
|
||||
<div id="title-container"><div dir="auto" id="subtitle"></div></div>`;
|
||||
|
||||
return document.registerElement('gaia-app-icon', { prototype: proto });
|
||||
})(window);
|
||||
54
gaia/shared/elements/gaia-site-icon/style.css
Executable file
|
|
@ -0,0 +1,54 @@
|
|||
gaia-app-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
#image-container {
|
||||
position: relative;
|
||||
transition: visibility 0.2s, opacity 0.2s;
|
||||
}
|
||||
|
||||
#image-container.initial-load {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#image-container > #spinner {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#image-container.downloading > #spinner {
|
||||
display: block;
|
||||
background: no-repeat center center url('images/app_install_spinner.svg');
|
||||
background-size: contain;
|
||||
animation: rotate 2s infinite linear;
|
||||
}
|
||||
|
||||
#image-container,
|
||||
#image-container > * {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#subtitle {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
#title-container {
|
||||
transition: opacity 0.2s, visibility 0.2s;
|
||||
}
|
||||
|
||||
#title-container.hidden {
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
height: 0;
|
||||
}
|
||||