VulpesOS/gaia/shared/elements/gaia_grid/js/items/mozapp.js
OverSu b38dea0597 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.
2026-09-30 23:13:03 +02:00

369 lines
9.9 KiB
JavaScript
Executable file

'use strict';
/* global ConfirmDialogHelper, GaiaGrid */
(function(exports) {
const IDENTIFIER_SEP = '-';
var MOZAPP_EVENTS = [
'downloadsuccess',
'downloaderror',
'downloadavailable',
'downloadapplied',
'progress'
];
var APP_LOADING = 'loading';
var APP_ERROR = 'error';
var APP_UNRECOVERABLE = 'unrecoverable';
var APP_PAUSED = 'paused';
var APP_READY = 'ready';
function localizeString(str) {
var userLang = document.documentElement.lang;
// We want to make sure that we translate 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;
}
/**
* Represents single app icon on the homepage.
*/
function Mozapp(app, entryPoint, details) {
this.app = app;
this.entryPoint = entryPoint;
this.detail = {
type: 'app',
manifestURL: app.manifestURL,
entryPoint: entryPoint,
index: 0,
// XXX: Somewhat ugly hack around the constructor args
decoratedIconBlob: details && details.decoratedIconBlob
};
// Yes this is clobbering all other listeners to the app MozApp
// should be in charge here otherwise the icon will be incorrect.
MOZAPP_EVENTS.forEach(function(type) {
this.app['on' + type] = this.handleEvent.bind(this);
}, this);
// Determine and set the initial state of the app when it is inserted.
this.setAppState(this._determineState(app));
}
/**
`app.manifest.role`s which should not be displayed on the grid.
*/
Mozapp.HIDDEN_ROLES = [
'system', 'input', 'homescreen', 'search', 'addon', 'langpack'
];
Mozapp.prototype = {
__proto__: GaiaGrid.GridItem.prototype,
/**
Determine if this application is supposed to be hidden.
@return Boolean
*/
_isHiddenRole: function() {
var manifest = this.app.manifest;
// don't know what the role is so it is not hidden
if (!manifest || !manifest.role) {
return null;
}
// if it is in the hidden role list return true
return Mozapp.HIDDEN_ROLES.indexOf(manifest.role) !== -1;
},
/**
Figure out which state the app is in
*/
_determineState: function(app) {
// is there is a pending download but we cannot download then this app is
// in an unrecoverable state due to some fatal error.
if (
// Must be pending (meaning not launchable)
app.installState === 'pending' &&
// Without any option for downloading the app
!app.downloadAvailable &&
// And not be currently applying a download
!app.readyToApplyDownload
) {
return APP_UNRECOVERABLE;
}
// If the app is currently downloading life is good and we should display
// loading.
if (app.downloading) {
return APP_LOADING;
}
// Bug 1027347 - downloadError is always present even if there is no error
// so we check both for the error and that the error has a
// name.
if (app.downloadError && app.downloadError.name) {
// If the app was paused while the homescreen was running the
// downloaderror event will fire with the DOWNLOAD_CANCELED error.
if (app.downloadError.name === 'DOWNLOAD_CANCELED') {
return APP_PAUSED;
}
return APP_ERROR;
}
// Note that this logic is correct because app.downloading is caught above
// separately.
if (app.installState === 'pending') {
return APP_PAUSED;
}
return APP_READY;
},
handleEvent: function(event) {
switch (event.type) {
case 'progress':
case 'downloaderror':
case 'downloadsuccess':
this.setAppState(this._determineState(this.app));
break;
case 'downloadapplied':
// Ensure that a hidden app has not somehow been added to the grid...
if (this._isHiddenRole()) {
console.warn('Removing hidden app from the grid', this.name);
return this.removeFromGrid();
}
// we may have updated icons so recalculate the correct icon.
delete this._accurateIcon;
delete this._accurateIconSize;
// Need to set app state here to correctly handle the pause/resume
// case.
this.setAppState(this._determineState(this.app));
// XXX: This introduces a potential race condition.
// GridItem.renderIcon is not concurrency safe one image may override
// another without ordering.
this.renderIcon();
window.dispatchEvent(new CustomEvent('downloadapplied', {
detail: {
id: this.app.manifestURL
}
}));
break;
}
},
// Overrides GridItem.fetchIconBlob
fetchIconBlob: function() {
if (!this._accurateIconSize) {
this._accurateIconSize =
this.closestIconSizeFromList(this.descriptor.icons);
}
var result = this._accurateIconSize ?
navigator.mozApps.mgmt.getIcon(this.app, this._accurateIconSize,
this.entryPoint || '') :
GaiaGrid.GridItem.prototype.fetchIconBlob.call(this);
return result;
},
/**
* Returns the height in pixels of each icon.
*/
get pixelHeight() {
return this.grid.layout.gridItemHeight;
},
/**
* Width in grid units for each icon.
*/
gridWidth: 1,
get name() {
var userLang = document.documentElement.lang;
if (navigator.mozL10n && userLang in navigator.mozL10n.qps) {
return navigator.mozL10n.qps[userLang].
translate(this.descriptor.short_name || this.descriptor.name);
}
var locales = this.descriptor.locales;
var localized =
locales && locales[userLang] &&
(locales[userLang].short_name || locales[userLang].name);
return localized || this.descriptor.short_name || this.descriptor.name;
},
asyncName: function() {
var userLang = document.documentElement.lang;
var ep = this.entryPoint || undefined;
if (!this.app.getLocalizedValue) {
return new Promise((resolve, reject) => { reject(); });
}
return this.app.getLocalizedValue('short_name', userLang, ep).then(
shortName => localizeString(shortName),
this.app.getLocalizedValue.bind(this.app, 'name', userLang, ep)).then(
name => localizeString(name),
() => this.name
);
},
/**
* Returns the icon image path.
*/
get icon() {
var icon = this._accurateIcon;
if (!icon) {
icon = this._accurateIcon = this.closestIconFromList(
this.descriptor.icons);
}
return icon;
},
get descriptor() {
var manifest = this.app.manifest || this.app.updateManifest;
if (this.entryPoint && manifest.entry_points &&
manifest.entry_points[this.entryPoint]) {
return manifest.entry_points[this.entryPoint];
}
return manifest;
},
identifierSeparator: IDENTIFIER_SEP,
get identifier() {
var identifier = [this.app.manifestURL];
if (this.entryPoint) {
identifier.push(this.entryPoint);
}
return identifier.join(IDENTIFIER_SEP);
},
/**
* Returns true if this app is removable.
*/
isRemovable: function() {
return this.app.removable;
},
/**
Show a dialog to handle unrecoverable errors.
*/
unrecoverableError: function() {
navigator.mozApps.mgmt.uninstall(this.app);
},
cancel: function() {
var dialog = new ConfirmDialogHelper({
type: 'pause',
title: {id: 'gaia-grid-stop-download-title', args: { name: this.name }},
body: 'gaia-grid-stop-download-body',
cancel: {
title: 'gaia-grid-cancel'
},
confirm: {
title: 'gaia-grid-stop-download-action',
type: 'danger',
cb: () => this.app.cancelDownload()
}
});
dialog.show(document.body);
},
resume: function() {
var dialog = new ConfirmDialogHelper({
type: 'resume',
title: 'gaia-grid-resume-download-title',
body: {id: 'gaia-grid-resume-download-body', args: { name: this.name }},
cancel: {
title: 'gaia-grid-cancel'
},
confirm: {
title: 'gaia-grid-resume-download-action',
cb: () => {
// enter the loading state optimistically
this.setAppState(APP_LOADING);
this.app.download();
}
}
});
dialog.show(document.body);
},
setAppState: function(state) {
this.appState = state;
if (this.element) {
this.element.dataset.appState = state;
}
},
render: function() {
// If there is no element before we can render we need to update the
// app state. Render may also be called to reposition elements.
var needsStateSet = !this.element;
GaiaGrid.GridItem.prototype.render.apply(this, arguments);
if (needsStateSet) {
// ensure the newly rendered item has an app state
this.setAppState(this.appState);
}
},
/**
* Resolves click action.
*/
launch: function() {
var app = this.app;
switch (this._determineState(app)) {
case APP_UNRECOVERABLE:
return this.unrecoverableError();
case APP_ERROR:
case APP_PAUSED:
return this.resume();
case APP_LOADING:
return this.cancel();
}
window.performance.mark('appLaunch@' + app.origin);
if (this.entryPoint) {
return app.launch(this.entryPoint);
}
// Default action is to launch the app.
return app.launch();
}
};
exports.GaiaGrid.Mozapp = Mozapp;
}(window));