gecko/browser/devtools/app-manager/content/projects.js

453 lines
14 KiB
JavaScript

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
const Cr = Components.results;
Cu.import("resource:///modules/devtools/gDevTools.jsm");
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {require} = devtools;
const {ConnectionManager, Connection} = require("devtools/client/connection-manager");
const {AppProjects} = require("devtools/app-manager/app-projects");
const {AppValidator} = require("devtools/app-manager/app-validator");
const {Services} = Cu.import("resource://gre/modules/Services.jsm");
const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
const {installHosted, installPackaged, getTargetForApp,
reloadApp, launchApp, closeApp} = require("devtools/app-actor-front");
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
const promise = require("devtools/toolkit/deprecated-sync-thenables");
const MANIFEST_EDITOR_ENABLED = "devtools.appmanager.manifestEditor.enabled";
window.addEventListener("message", function(event) {
try {
let json = JSON.parse(event.data);
if (json.name == "connection") {
let cid = parseInt(json.cid);
for (let c of ConnectionManager.connections) {
if (c.uid == cid) {
UI.connection = c;
UI.onNewConnection();
break;
}
}
}
} catch(e) {}
});
window.addEventListener("unload", function onUnload() {
window.removeEventListener("unload", onUnload);
UI.destroy();
});
let UI = {
isReady: false,
onload: function() {
if (Services.prefs.getBoolPref(MANIFEST_EDITOR_ENABLED)) {
document.querySelector("#lense").setAttribute("manifest-editable", "");
}
this.template = new Template(document.body, AppProjects.store, Utils.l10n);
this.template.start();
AppProjects.load().then(() => {
AppProjects.store.object.projects.forEach(UI.validate);
this.isReady = true;
this.emit("ready");
});
},
destroy: function() {
if (this.connection) {
this.connection.off(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange);
}
this.template.destroy();
},
onNewConnection: function() {
this.connection.on(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange);
this._onConnectionStatusChange();
},
_onConnectionStatusChange: function() {
if (this.connection.status != Connection.Status.CONNECTED) {
document.body.classList.remove("connected");
this.listTabsResponse = null;
} else {
document.body.classList.add("connected");
this.connection.client.listTabs(
response => {this.listTabsResponse = response}
);
}
},
get connected() { return !!this.listTabsResponse; },
_selectFolder: function() {
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
fp.init(window, Utils.l10n("project.filePickerTitle"), Ci.nsIFilePicker.modeGetFolder);
let res = fp.show();
if (res != Ci.nsIFilePicker.returnCancel)
return fp.file;
return null;
},
addPackaged: function(folder) {
if (!folder) {
folder = this._selectFolder();
}
if (!folder)
return;
return AppProjects.addPackaged(folder)
.then(function (project) {
UI.validate(project);
UI.selectProject(project.location);
});
},
addHosted: function() {
let form = document.querySelector("#new-hosted-project-wrapper");
if (!form.checkValidity())
return;
let urlInput = document.querySelector("#url-input");
let manifestURL = urlInput.value;
return AppProjects.addHosted(manifestURL)
.then(function (project) {
UI.validate(project);
UI.selectProject(project.location);
});
},
_getLocalIconURL: function(project, manifest) {
let icon;
if (manifest.icons) {
let size = Object.keys(manifest.icons).sort(function(a, b) b - a)[0];
if (size) {
icon = manifest.icons[size];
}
}
if (!icon)
return "chrome://browser/skin/devtools/app-manager/default-app-icon.png";
if (project.type == "hosted") {
let manifestURL = Services.io.newURI(project.location, null, null);
let origin = Services.io.newURI(manifestURL.prePath, null, null);
return Services.io.newURI(icon, null, origin).spec;
} else if (project.type == "packaged") {
let projectFolder = FileUtils.File(project.location);
let folderURI = Services.io.newFileURI(projectFolder).spec;
return folderURI + icon.replace(/^\/|\\/, "");
}
},
validate: function(project) {
let validation = new AppValidator(project);
return validation.validate()
.then(function () {
if (validation.manifest) {
project.icon = UI._getLocalIconURL(project, validation.manifest);
project.manifest = validation.manifest;
}
project.validationStatus = "valid";
if (validation.warnings.length > 0) {
project.warningsCount = validation.warnings.length;
project.warnings = validation.warnings.join(",\n ");
project.validationStatus = "warning";
} else {
project.warnings = "";
project.warningsCount = 0;
}
if (validation.errors.length > 0) {
project.errorsCount = validation.errors.length;
project.errors = validation.errors.join(",\n ");
project.validationStatus = "error";
} else {
project.errors = "";
project.errorsCount = 0;
}
if (project.warningsCount && project.errorsCount) {
project.validationStatus = "error warning";
}
});
},
update: function(button, location) {
button.disabled = true;
let project = AppProjects.get(location);
// Update the manifest editor view, in case the manifest was modified
// outside of the app manager. This can happen in parallel with the other
// steps.
this._showManifestEditor(project);
this.validate(project)
.then(() => {
// Install the app to the device if we are connected,
// and there is no error
if (project.errorsCount == 0 && this.connected) {
return this.install(project);
}
})
.then(() => {
button.disabled = false;
// Finally try to reload the app if it is already opened
if (this.connected) {
this.reload(project);
}
},
(res) => {
button.disabled = false;
let message = res.error + ": " + res.message;
alert(message);
this.connection.log(message);
});
},
saveManifest: function(button) {
button.disabled = true;
this.manifestEditor.save().then(() => button.disabled = false);
},
reload: function (project) {
if (!this.connected) {
return promise.reject();
}
return reloadApp(this.connection.client,
this.listTabsResponse.webappsActor,
this._getProjectManifestURL(project)).
then(() => {
this.connection.log("App reloaded");
});
},
remove: function(location, event) {
if (event) {
// We don't want the "click" event to be propagated to the project item.
// That would trigger `selectProject()`.
event.stopPropagation();
}
let item = document.getElementById(location);
let toSelect = document.querySelector(".project-item.selected");
toSelect = toSelect ? toSelect.id : "";
if (toSelect == location) {
toSelect = null;
let sibling;
if (item.previousElementSibling) {
sibling = item.previousElementSibling;
} else {
sibling = item.nextElementSibling;
}
if (sibling && !!AppProjects.get(sibling.id)) {
toSelect = sibling.id;
}
}
AppProjects.remove(location).then(() => {
this.selectProject(toSelect);
});
},
_getProjectManifestURL: function (project) {
if (project.type == "packaged") {
return "app://" + project.packagedAppOrigin + "/manifest.webapp";
} else if (project.type == "hosted") {
return project.location;
}
},
install: function(project) {
if (!this.connected) {
return promise.reject();
}
this.connection.log("Installing the " + project.manifest.name + " app...");
let installPromise;
if (project.type == "packaged") {
installPromise = installPackaged(this.connection.client, this.listTabsResponse.webappsActor, project.location, project.packagedAppOrigin)
.then(({ appId }) => {
// If the packaged app specified a custom origin override,
// we need to update the local project origin
project.packagedAppOrigin = appId;
// And ensure the indexed db on disk is also updated
AppProjects.update(project);
});
} else {
let manifestURLObject = Services.io.newURI(project.location, null, null);
let origin = Services.io.newURI(manifestURLObject.prePath, null, null);
let appId = origin.host;
let metadata = {
origin: origin.spec,
manifestURL: project.location
};
installPromise = installHosted(this.connection.client, this.listTabsResponse.webappsActor, appId, metadata, project.manifest);
}
installPromise.then(() => {
this.connection.log("Install completed.");
}, () => {
this.connection.log("Install failed.");
});
return installPromise;
},
start: function(project) {
if (!this.connected) {
return promise.reject();
}
let manifestURL = this._getProjectManifestURL(project);
return launchApp(this.connection.client,
this.listTabsResponse.webappsActor,
manifestURL);
},
stop: function(location) {
if (!this.connected) {
return promise.reject();
}
let project = AppProjects.get(location);
let manifestURL = this._getProjectManifestURL(project);
return closeApp(this.connection.client,
this.listTabsResponse.webappsActor,
manifestURL);
},
debug: function(button, location) {
if (!this.connected) {
return promise.reject();
}
button.disabled = true;
let project = AppProjects.get(location);
let onFailedToStart = (error) => {
// If not installed, install and open it
if (error == "NO_SUCH_APP") {
return this.install(project);
} else {
throw error;
}
};
let onStarted = () => {
// Once we asked the app to launch, the app isn't necessary completely loaded.
// launch request only ask the app to launch and immediatly returns.
// We have to keep trying to get app tab actors required to create its target.
let deferred = promise.defer();
let loop = (count) => {
// Ensure not looping for ever
if (count >= 100) {
deferred.reject("Unable to connect to the app");
return;
}
// Also, in case the app wasn't installed yet, we also have to keep asking the
// app to launch, as launch request made right after install may race.
this.start(project);
getTargetForApp(
this.connection.client,
this.listTabsResponse.webappsActor,
this._getProjectManifestURL(project)).
then(deferred.resolve,
(err) => {
if (err == "appNotFound")
setTimeout(loop, 500, count + 1);
else
deferred.reject(err);
});
};
loop(0);
return deferred.promise;
};
// First try to open the app
this.start(project)
.then(null, onFailedToStart)
.then(onStarted)
.then((target) =>
top.UI.openAndShowToolboxForTarget(target,
project.manifest.name,
project.icon))
.then(() => {
// And only when the toolbox is opened, release the button
button.disabled = false;
},
(err) => {
button.disabled = false;
let message = err.error ? err.error + ": " + err.message : String(err);
alert(message);
this.connection.log(message);
});
},
reveal: function(location) {
let project = AppProjects.get(location);
if (project.type == "packaged") {
let projectFolder = FileUtils.File(project.location);
projectFolder.reveal();
} else {
// TODO: eventually open hosted apps in firefox
// when permissions are correctly supported by firefox
}
},
selectProject: function(location) {
let projects = AppProjects.store.object.projects;
let idx = 0;
for (; idx < projects.length; idx++) {
if (projects[idx].location == location) {
break;
}
}
let oldButton = document.querySelector(".project-item.selected");
if (oldButton) {
oldButton.classList.remove("selected");
}
if (idx == projects.length) {
// Not found. Empty lense.
let lense = document.querySelector("#lense");
lense.setAttribute("template-for", '{"path":"","childSelector":""}');
this.template._processFor(lense);
return;
}
let button = document.getElementById(location);
button.classList.add("selected");
let template = '{"path":"projects.' + idx + '","childSelector":"#lense-template"}';
let lense = document.querySelector("#lense");
lense.setAttribute("template-for", template);
this.template._processFor(lense);
let project = projects[idx];
this._showManifestEditor(project).then(() => this.emit("project-selected"));
},
_showManifestEditor: function(project) {
if (this.manifestEditor) {
this.manifestEditor.destroy();
}
let editorContainer = document.querySelector("#lense .manifest-editor");
this.manifestEditor = new ManifestEditor(project);
return this.manifestEditor.show(editorContainer);
}
};
// This must be bound immediately, as it might be used via the message listener
// before UI.onload() has been called.
UI._onConnectionStatusChange = UI._onConnectionStatusChange.bind(UI);
EventEmitter.decorate(UI);