diff -r 000000000000 -r 6474c204b198 browser/devtools/app-manager/content/projects.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/devtools/app-manager/content/projects.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,452 @@ +/* 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);