1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/app-manager/content/projects.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,452 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +const Cc = Components.classes; 1.9 +const Ci = Components.interfaces; 1.10 +const Cu = Components.utils; 1.11 +const Cr = Components.results; 1.12 +Cu.import("resource:///modules/devtools/gDevTools.jsm"); 1.13 +const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); 1.14 +const {require} = devtools; 1.15 +const {ConnectionManager, Connection} = require("devtools/client/connection-manager"); 1.16 +const {AppProjects} = require("devtools/app-manager/app-projects"); 1.17 +const {AppValidator} = require("devtools/app-manager/app-validator"); 1.18 +const {Services} = Cu.import("resource://gre/modules/Services.jsm"); 1.19 +const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm"); 1.20 +const {installHosted, installPackaged, getTargetForApp, 1.21 + reloadApp, launchApp, closeApp} = require("devtools/app-actor-front"); 1.22 +const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js"); 1.23 + 1.24 +const promise = require("devtools/toolkit/deprecated-sync-thenables"); 1.25 + 1.26 +const MANIFEST_EDITOR_ENABLED = "devtools.appmanager.manifestEditor.enabled"; 1.27 + 1.28 +window.addEventListener("message", function(event) { 1.29 + try { 1.30 + let json = JSON.parse(event.data); 1.31 + if (json.name == "connection") { 1.32 + let cid = parseInt(json.cid); 1.33 + for (let c of ConnectionManager.connections) { 1.34 + if (c.uid == cid) { 1.35 + UI.connection = c; 1.36 + UI.onNewConnection(); 1.37 + break; 1.38 + } 1.39 + } 1.40 + } 1.41 + } catch(e) {} 1.42 +}); 1.43 + 1.44 +window.addEventListener("unload", function onUnload() { 1.45 + window.removeEventListener("unload", onUnload); 1.46 + UI.destroy(); 1.47 +}); 1.48 + 1.49 +let UI = { 1.50 + isReady: false, 1.51 + 1.52 + onload: function() { 1.53 + if (Services.prefs.getBoolPref(MANIFEST_EDITOR_ENABLED)) { 1.54 + document.querySelector("#lense").setAttribute("manifest-editable", ""); 1.55 + } 1.56 + 1.57 + this.template = new Template(document.body, AppProjects.store, Utils.l10n); 1.58 + this.template.start(); 1.59 + 1.60 + AppProjects.load().then(() => { 1.61 + AppProjects.store.object.projects.forEach(UI.validate); 1.62 + this.isReady = true; 1.63 + this.emit("ready"); 1.64 + }); 1.65 + }, 1.66 + 1.67 + destroy: function() { 1.68 + if (this.connection) { 1.69 + this.connection.off(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange); 1.70 + } 1.71 + this.template.destroy(); 1.72 + }, 1.73 + 1.74 + onNewConnection: function() { 1.75 + this.connection.on(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange); 1.76 + this._onConnectionStatusChange(); 1.77 + }, 1.78 + 1.79 + _onConnectionStatusChange: function() { 1.80 + if (this.connection.status != Connection.Status.CONNECTED) { 1.81 + document.body.classList.remove("connected"); 1.82 + this.listTabsResponse = null; 1.83 + } else { 1.84 + document.body.classList.add("connected"); 1.85 + this.connection.client.listTabs( 1.86 + response => {this.listTabsResponse = response} 1.87 + ); 1.88 + } 1.89 + }, 1.90 + 1.91 + get connected() { return !!this.listTabsResponse; }, 1.92 + 1.93 + _selectFolder: function() { 1.94 + let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); 1.95 + fp.init(window, Utils.l10n("project.filePickerTitle"), Ci.nsIFilePicker.modeGetFolder); 1.96 + let res = fp.show(); 1.97 + if (res != Ci.nsIFilePicker.returnCancel) 1.98 + return fp.file; 1.99 + return null; 1.100 + }, 1.101 + 1.102 + addPackaged: function(folder) { 1.103 + if (!folder) { 1.104 + folder = this._selectFolder(); 1.105 + } 1.106 + if (!folder) 1.107 + return; 1.108 + return AppProjects.addPackaged(folder) 1.109 + .then(function (project) { 1.110 + UI.validate(project); 1.111 + UI.selectProject(project.location); 1.112 + }); 1.113 + }, 1.114 + 1.115 + addHosted: function() { 1.116 + let form = document.querySelector("#new-hosted-project-wrapper"); 1.117 + if (!form.checkValidity()) 1.118 + return; 1.119 + 1.120 + let urlInput = document.querySelector("#url-input"); 1.121 + let manifestURL = urlInput.value; 1.122 + return AppProjects.addHosted(manifestURL) 1.123 + .then(function (project) { 1.124 + UI.validate(project); 1.125 + UI.selectProject(project.location); 1.126 + }); 1.127 + }, 1.128 + 1.129 + _getLocalIconURL: function(project, manifest) { 1.130 + let icon; 1.131 + if (manifest.icons) { 1.132 + let size = Object.keys(manifest.icons).sort(function(a, b) b - a)[0]; 1.133 + if (size) { 1.134 + icon = manifest.icons[size]; 1.135 + } 1.136 + } 1.137 + if (!icon) 1.138 + return "chrome://browser/skin/devtools/app-manager/default-app-icon.png"; 1.139 + if (project.type == "hosted") { 1.140 + let manifestURL = Services.io.newURI(project.location, null, null); 1.141 + let origin = Services.io.newURI(manifestURL.prePath, null, null); 1.142 + return Services.io.newURI(icon, null, origin).spec; 1.143 + } else if (project.type == "packaged") { 1.144 + let projectFolder = FileUtils.File(project.location); 1.145 + let folderURI = Services.io.newFileURI(projectFolder).spec; 1.146 + return folderURI + icon.replace(/^\/|\\/, ""); 1.147 + } 1.148 + }, 1.149 + 1.150 + validate: function(project) { 1.151 + let validation = new AppValidator(project); 1.152 + return validation.validate() 1.153 + .then(function () { 1.154 + if (validation.manifest) { 1.155 + project.icon = UI._getLocalIconURL(project, validation.manifest); 1.156 + project.manifest = validation.manifest; 1.157 + } 1.158 + 1.159 + project.validationStatus = "valid"; 1.160 + 1.161 + if (validation.warnings.length > 0) { 1.162 + project.warningsCount = validation.warnings.length; 1.163 + project.warnings = validation.warnings.join(",\n "); 1.164 + project.validationStatus = "warning"; 1.165 + } else { 1.166 + project.warnings = ""; 1.167 + project.warningsCount = 0; 1.168 + } 1.169 + 1.170 + if (validation.errors.length > 0) { 1.171 + project.errorsCount = validation.errors.length; 1.172 + project.errors = validation.errors.join(",\n "); 1.173 + project.validationStatus = "error"; 1.174 + } else { 1.175 + project.errors = ""; 1.176 + project.errorsCount = 0; 1.177 + } 1.178 + 1.179 + if (project.warningsCount && project.errorsCount) { 1.180 + project.validationStatus = "error warning"; 1.181 + } 1.182 + 1.183 + }); 1.184 + 1.185 + }, 1.186 + 1.187 + update: function(button, location) { 1.188 + button.disabled = true; 1.189 + let project = AppProjects.get(location); 1.190 + 1.191 + // Update the manifest editor view, in case the manifest was modified 1.192 + // outside of the app manager. This can happen in parallel with the other 1.193 + // steps. 1.194 + this._showManifestEditor(project); 1.195 + 1.196 + this.validate(project) 1.197 + .then(() => { 1.198 + // Install the app to the device if we are connected, 1.199 + // and there is no error 1.200 + if (project.errorsCount == 0 && this.connected) { 1.201 + return this.install(project); 1.202 + } 1.203 + }) 1.204 + .then(() => { 1.205 + button.disabled = false; 1.206 + // Finally try to reload the app if it is already opened 1.207 + if (this.connected) { 1.208 + this.reload(project); 1.209 + } 1.210 + }, 1.211 + (res) => { 1.212 + button.disabled = false; 1.213 + let message = res.error + ": " + res.message; 1.214 + alert(message); 1.215 + this.connection.log(message); 1.216 + }); 1.217 + }, 1.218 + 1.219 + saveManifest: function(button) { 1.220 + button.disabled = true; 1.221 + this.manifestEditor.save().then(() => button.disabled = false); 1.222 + }, 1.223 + 1.224 + reload: function (project) { 1.225 + if (!this.connected) { 1.226 + return promise.reject(); 1.227 + } 1.228 + return reloadApp(this.connection.client, 1.229 + this.listTabsResponse.webappsActor, 1.230 + this._getProjectManifestURL(project)). 1.231 + then(() => { 1.232 + this.connection.log("App reloaded"); 1.233 + }); 1.234 + }, 1.235 + 1.236 + remove: function(location, event) { 1.237 + if (event) { 1.238 + // We don't want the "click" event to be propagated to the project item. 1.239 + // That would trigger `selectProject()`. 1.240 + event.stopPropagation(); 1.241 + } 1.242 + 1.243 + let item = document.getElementById(location); 1.244 + 1.245 + let toSelect = document.querySelector(".project-item.selected"); 1.246 + toSelect = toSelect ? toSelect.id : ""; 1.247 + 1.248 + if (toSelect == location) { 1.249 + toSelect = null; 1.250 + let sibling; 1.251 + if (item.previousElementSibling) { 1.252 + sibling = item.previousElementSibling; 1.253 + } else { 1.254 + sibling = item.nextElementSibling; 1.255 + } 1.256 + if (sibling && !!AppProjects.get(sibling.id)) { 1.257 + toSelect = sibling.id; 1.258 + } 1.259 + } 1.260 + 1.261 + AppProjects.remove(location).then(() => { 1.262 + this.selectProject(toSelect); 1.263 + }); 1.264 + }, 1.265 + 1.266 + _getProjectManifestURL: function (project) { 1.267 + if (project.type == "packaged") { 1.268 + return "app://" + project.packagedAppOrigin + "/manifest.webapp"; 1.269 + } else if (project.type == "hosted") { 1.270 + return project.location; 1.271 + } 1.272 + }, 1.273 + 1.274 + install: function(project) { 1.275 + if (!this.connected) { 1.276 + return promise.reject(); 1.277 + } 1.278 + this.connection.log("Installing the " + project.manifest.name + " app..."); 1.279 + let installPromise; 1.280 + if (project.type == "packaged") { 1.281 + installPromise = installPackaged(this.connection.client, this.listTabsResponse.webappsActor, project.location, project.packagedAppOrigin) 1.282 + .then(({ appId }) => { 1.283 + // If the packaged app specified a custom origin override, 1.284 + // we need to update the local project origin 1.285 + project.packagedAppOrigin = appId; 1.286 + // And ensure the indexed db on disk is also updated 1.287 + AppProjects.update(project); 1.288 + }); 1.289 + } else { 1.290 + let manifestURLObject = Services.io.newURI(project.location, null, null); 1.291 + let origin = Services.io.newURI(manifestURLObject.prePath, null, null); 1.292 + let appId = origin.host; 1.293 + let metadata = { 1.294 + origin: origin.spec, 1.295 + manifestURL: project.location 1.296 + }; 1.297 + installPromise = installHosted(this.connection.client, this.listTabsResponse.webappsActor, appId, metadata, project.manifest); 1.298 + } 1.299 + 1.300 + installPromise.then(() => { 1.301 + this.connection.log("Install completed."); 1.302 + }, () => { 1.303 + this.connection.log("Install failed."); 1.304 + }); 1.305 + 1.306 + return installPromise; 1.307 + }, 1.308 + 1.309 + start: function(project) { 1.310 + if (!this.connected) { 1.311 + return promise.reject(); 1.312 + } 1.313 + let manifestURL = this._getProjectManifestURL(project); 1.314 + return launchApp(this.connection.client, 1.315 + this.listTabsResponse.webappsActor, 1.316 + manifestURL); 1.317 + }, 1.318 + 1.319 + stop: function(location) { 1.320 + if (!this.connected) { 1.321 + return promise.reject(); 1.322 + } 1.323 + let project = AppProjects.get(location); 1.324 + let manifestURL = this._getProjectManifestURL(project); 1.325 + return closeApp(this.connection.client, 1.326 + this.listTabsResponse.webappsActor, 1.327 + manifestURL); 1.328 + }, 1.329 + 1.330 + debug: function(button, location) { 1.331 + if (!this.connected) { 1.332 + return promise.reject(); 1.333 + } 1.334 + button.disabled = true; 1.335 + let project = AppProjects.get(location); 1.336 + 1.337 + let onFailedToStart = (error) => { 1.338 + // If not installed, install and open it 1.339 + if (error == "NO_SUCH_APP") { 1.340 + return this.install(project); 1.341 + } else { 1.342 + throw error; 1.343 + } 1.344 + }; 1.345 + let onStarted = () => { 1.346 + // Once we asked the app to launch, the app isn't necessary completely loaded. 1.347 + // launch request only ask the app to launch and immediatly returns. 1.348 + // We have to keep trying to get app tab actors required to create its target. 1.349 + let deferred = promise.defer(); 1.350 + let loop = (count) => { 1.351 + // Ensure not looping for ever 1.352 + if (count >= 100) { 1.353 + deferred.reject("Unable to connect to the app"); 1.354 + return; 1.355 + } 1.356 + // Also, in case the app wasn't installed yet, we also have to keep asking the 1.357 + // app to launch, as launch request made right after install may race. 1.358 + this.start(project); 1.359 + getTargetForApp( 1.360 + this.connection.client, 1.361 + this.listTabsResponse.webappsActor, 1.362 + this._getProjectManifestURL(project)). 1.363 + then(deferred.resolve, 1.364 + (err) => { 1.365 + if (err == "appNotFound") 1.366 + setTimeout(loop, 500, count + 1); 1.367 + else 1.368 + deferred.reject(err); 1.369 + }); 1.370 + }; 1.371 + loop(0); 1.372 + return deferred.promise; 1.373 + }; 1.374 + 1.375 + // First try to open the app 1.376 + this.start(project) 1.377 + .then(null, onFailedToStart) 1.378 + .then(onStarted) 1.379 + .then((target) => 1.380 + top.UI.openAndShowToolboxForTarget(target, 1.381 + project.manifest.name, 1.382 + project.icon)) 1.383 + .then(() => { 1.384 + // And only when the toolbox is opened, release the button 1.385 + button.disabled = false; 1.386 + }, 1.387 + (err) => { 1.388 + button.disabled = false; 1.389 + let message = err.error ? err.error + ": " + err.message : String(err); 1.390 + alert(message); 1.391 + this.connection.log(message); 1.392 + }); 1.393 + }, 1.394 + 1.395 + reveal: function(location) { 1.396 + let project = AppProjects.get(location); 1.397 + if (project.type == "packaged") { 1.398 + let projectFolder = FileUtils.File(project.location); 1.399 + projectFolder.reveal(); 1.400 + } else { 1.401 + // TODO: eventually open hosted apps in firefox 1.402 + // when permissions are correctly supported by firefox 1.403 + } 1.404 + }, 1.405 + 1.406 + selectProject: function(location) { 1.407 + let projects = AppProjects.store.object.projects; 1.408 + let idx = 0; 1.409 + for (; idx < projects.length; idx++) { 1.410 + if (projects[idx].location == location) { 1.411 + break; 1.412 + } 1.413 + } 1.414 + 1.415 + let oldButton = document.querySelector(".project-item.selected"); 1.416 + if (oldButton) { 1.417 + oldButton.classList.remove("selected"); 1.418 + } 1.419 + 1.420 + if (idx == projects.length) { 1.421 + // Not found. Empty lense. 1.422 + let lense = document.querySelector("#lense"); 1.423 + lense.setAttribute("template-for", '{"path":"","childSelector":""}'); 1.424 + this.template._processFor(lense); 1.425 + return; 1.426 + } 1.427 + 1.428 + let button = document.getElementById(location); 1.429 + button.classList.add("selected"); 1.430 + 1.431 + let template = '{"path":"projects.' + idx + '","childSelector":"#lense-template"}'; 1.432 + 1.433 + let lense = document.querySelector("#lense"); 1.434 + lense.setAttribute("template-for", template); 1.435 + this.template._processFor(lense); 1.436 + 1.437 + let project = projects[idx]; 1.438 + this._showManifestEditor(project).then(() => this.emit("project-selected")); 1.439 + }, 1.440 + 1.441 + _showManifestEditor: function(project) { 1.442 + if (this.manifestEditor) { 1.443 + this.manifestEditor.destroy(); 1.444 + } 1.445 + let editorContainer = document.querySelector("#lense .manifest-editor"); 1.446 + this.manifestEditor = new ManifestEditor(project); 1.447 + return this.manifestEditor.show(editorContainer); 1.448 + } 1.449 +}; 1.450 + 1.451 +// This must be bound immediately, as it might be used via the message listener 1.452 +// before UI.onload() has been called. 1.453 +UI._onConnectionStatusChange = UI._onConnectionStatusChange.bind(UI); 1.454 + 1.455 +EventEmitter.decorate(UI);