Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
369 lines
9.9 KiB
JavaScript
Executable file
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));
|