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:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View 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);