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

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 const Cc = Components.classes;
michael@0 6 const Ci = Components.interfaces;
michael@0 7 const Cu = Components.utils;
michael@0 8 const Cr = Components.results;
michael@0 9 Cu.import("resource:///modules/devtools/gDevTools.jsm");
michael@0 10 const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
michael@0 11 const {require} = devtools;
michael@0 12 const {ConnectionManager, Connection} = require("devtools/client/connection-manager");
michael@0 13 const {AppProjects} = require("devtools/app-manager/app-projects");
michael@0 14 const {AppValidator} = require("devtools/app-manager/app-validator");
michael@0 15 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
michael@0 16 const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
michael@0 17 const {installHosted, installPackaged, getTargetForApp,
michael@0 18 reloadApp, launchApp, closeApp} = require("devtools/app-actor-front");
michael@0 19 const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
michael@0 20
michael@0 21 const promise = require("devtools/toolkit/deprecated-sync-thenables");
michael@0 22
michael@0 23 const MANIFEST_EDITOR_ENABLED = "devtools.appmanager.manifestEditor.enabled";
michael@0 24
michael@0 25 window.addEventListener("message", function(event) {
michael@0 26 try {
michael@0 27 let json = JSON.parse(event.data);
michael@0 28 if (json.name == "connection") {
michael@0 29 let cid = parseInt(json.cid);
michael@0 30 for (let c of ConnectionManager.connections) {
michael@0 31 if (c.uid == cid) {
michael@0 32 UI.connection = c;
michael@0 33 UI.onNewConnection();
michael@0 34 break;
michael@0 35 }
michael@0 36 }
michael@0 37 }
michael@0 38 } catch(e) {}
michael@0 39 });
michael@0 40
michael@0 41 window.addEventListener("unload", function onUnload() {
michael@0 42 window.removeEventListener("unload", onUnload);
michael@0 43 UI.destroy();
michael@0 44 });
michael@0 45
michael@0 46 let UI = {
michael@0 47 isReady: false,
michael@0 48
michael@0 49 onload: function() {
michael@0 50 if (Services.prefs.getBoolPref(MANIFEST_EDITOR_ENABLED)) {
michael@0 51 document.querySelector("#lense").setAttribute("manifest-editable", "");
michael@0 52 }
michael@0 53
michael@0 54 this.template = new Template(document.body, AppProjects.store, Utils.l10n);
michael@0 55 this.template.start();
michael@0 56
michael@0 57 AppProjects.load().then(() => {
michael@0 58 AppProjects.store.object.projects.forEach(UI.validate);
michael@0 59 this.isReady = true;
michael@0 60 this.emit("ready");
michael@0 61 });
michael@0 62 },
michael@0 63
michael@0 64 destroy: function() {
michael@0 65 if (this.connection) {
michael@0 66 this.connection.off(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange);
michael@0 67 }
michael@0 68 this.template.destroy();
michael@0 69 },
michael@0 70
michael@0 71 onNewConnection: function() {
michael@0 72 this.connection.on(Connection.Events.STATUS_CHANGED, this._onConnectionStatusChange);
michael@0 73 this._onConnectionStatusChange();
michael@0 74 },
michael@0 75
michael@0 76 _onConnectionStatusChange: function() {
michael@0 77 if (this.connection.status != Connection.Status.CONNECTED) {
michael@0 78 document.body.classList.remove("connected");
michael@0 79 this.listTabsResponse = null;
michael@0 80 } else {
michael@0 81 document.body.classList.add("connected");
michael@0 82 this.connection.client.listTabs(
michael@0 83 response => {this.listTabsResponse = response}
michael@0 84 );
michael@0 85 }
michael@0 86 },
michael@0 87
michael@0 88 get connected() { return !!this.listTabsResponse; },
michael@0 89
michael@0 90 _selectFolder: function() {
michael@0 91 let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
michael@0 92 fp.init(window, Utils.l10n("project.filePickerTitle"), Ci.nsIFilePicker.modeGetFolder);
michael@0 93 let res = fp.show();
michael@0 94 if (res != Ci.nsIFilePicker.returnCancel)
michael@0 95 return fp.file;
michael@0 96 return null;
michael@0 97 },
michael@0 98
michael@0 99 addPackaged: function(folder) {
michael@0 100 if (!folder) {
michael@0 101 folder = this._selectFolder();
michael@0 102 }
michael@0 103 if (!folder)
michael@0 104 return;
michael@0 105 return AppProjects.addPackaged(folder)
michael@0 106 .then(function (project) {
michael@0 107 UI.validate(project);
michael@0 108 UI.selectProject(project.location);
michael@0 109 });
michael@0 110 },
michael@0 111
michael@0 112 addHosted: function() {
michael@0 113 let form = document.querySelector("#new-hosted-project-wrapper");
michael@0 114 if (!form.checkValidity())
michael@0 115 return;
michael@0 116
michael@0 117 let urlInput = document.querySelector("#url-input");
michael@0 118 let manifestURL = urlInput.value;
michael@0 119 return AppProjects.addHosted(manifestURL)
michael@0 120 .then(function (project) {
michael@0 121 UI.validate(project);
michael@0 122 UI.selectProject(project.location);
michael@0 123 });
michael@0 124 },
michael@0 125
michael@0 126 _getLocalIconURL: function(project, manifest) {
michael@0 127 let icon;
michael@0 128 if (manifest.icons) {
michael@0 129 let size = Object.keys(manifest.icons).sort(function(a, b) b - a)[0];
michael@0 130 if (size) {
michael@0 131 icon = manifest.icons[size];
michael@0 132 }
michael@0 133 }
michael@0 134 if (!icon)
michael@0 135 return "chrome://browser/skin/devtools/app-manager/default-app-icon.png";
michael@0 136 if (project.type == "hosted") {
michael@0 137 let manifestURL = Services.io.newURI(project.location, null, null);
michael@0 138 let origin = Services.io.newURI(manifestURL.prePath, null, null);
michael@0 139 return Services.io.newURI(icon, null, origin).spec;
michael@0 140 } else if (project.type == "packaged") {
michael@0 141 let projectFolder = FileUtils.File(project.location);
michael@0 142 let folderURI = Services.io.newFileURI(projectFolder).spec;
michael@0 143 return folderURI + icon.replace(/^\/|\\/, "");
michael@0 144 }
michael@0 145 },
michael@0 146
michael@0 147 validate: function(project) {
michael@0 148 let validation = new AppValidator(project);
michael@0 149 return validation.validate()
michael@0 150 .then(function () {
michael@0 151 if (validation.manifest) {
michael@0 152 project.icon = UI._getLocalIconURL(project, validation.manifest);
michael@0 153 project.manifest = validation.manifest;
michael@0 154 }
michael@0 155
michael@0 156 project.validationStatus = "valid";
michael@0 157
michael@0 158 if (validation.warnings.length > 0) {
michael@0 159 project.warningsCount = validation.warnings.length;
michael@0 160 project.warnings = validation.warnings.join(",\n ");
michael@0 161 project.validationStatus = "warning";
michael@0 162 } else {
michael@0 163 project.warnings = "";
michael@0 164 project.warningsCount = 0;
michael@0 165 }
michael@0 166
michael@0 167 if (validation.errors.length > 0) {
michael@0 168 project.errorsCount = validation.errors.length;
michael@0 169 project.errors = validation.errors.join(",\n ");
michael@0 170 project.validationStatus = "error";
michael@0 171 } else {
michael@0 172 project.errors = "";
michael@0 173 project.errorsCount = 0;
michael@0 174 }
michael@0 175
michael@0 176 if (project.warningsCount && project.errorsCount) {
michael@0 177 project.validationStatus = "error warning";
michael@0 178 }
michael@0 179
michael@0 180 });
michael@0 181
michael@0 182 },
michael@0 183
michael@0 184 update: function(button, location) {
michael@0 185 button.disabled = true;
michael@0 186 let project = AppProjects.get(location);
michael@0 187
michael@0 188 // Update the manifest editor view, in case the manifest was modified
michael@0 189 // outside of the app manager. This can happen in parallel with the other
michael@0 190 // steps.
michael@0 191 this._showManifestEditor(project);
michael@0 192
michael@0 193 this.validate(project)
michael@0 194 .then(() => {
michael@0 195 // Install the app to the device if we are connected,
michael@0 196 // and there is no error
michael@0 197 if (project.errorsCount == 0 && this.connected) {
michael@0 198 return this.install(project);
michael@0 199 }
michael@0 200 })
michael@0 201 .then(() => {
michael@0 202 button.disabled = false;
michael@0 203 // Finally try to reload the app if it is already opened
michael@0 204 if (this.connected) {
michael@0 205 this.reload(project);
michael@0 206 }
michael@0 207 },
michael@0 208 (res) => {
michael@0 209 button.disabled = false;
michael@0 210 let message = res.error + ": " + res.message;
michael@0 211 alert(message);
michael@0 212 this.connection.log(message);
michael@0 213 });
michael@0 214 },
michael@0 215
michael@0 216 saveManifest: function(button) {
michael@0 217 button.disabled = true;
michael@0 218 this.manifestEditor.save().then(() => button.disabled = false);
michael@0 219 },
michael@0 220
michael@0 221 reload: function (project) {
michael@0 222 if (!this.connected) {
michael@0 223 return promise.reject();
michael@0 224 }
michael@0 225 return reloadApp(this.connection.client,
michael@0 226 this.listTabsResponse.webappsActor,
michael@0 227 this._getProjectManifestURL(project)).
michael@0 228 then(() => {
michael@0 229 this.connection.log("App reloaded");
michael@0 230 });
michael@0 231 },
michael@0 232
michael@0 233 remove: function(location, event) {
michael@0 234 if (event) {
michael@0 235 // We don't want the "click" event to be propagated to the project item.
michael@0 236 // That would trigger `selectProject()`.
michael@0 237 event.stopPropagation();
michael@0 238 }
michael@0 239
michael@0 240 let item = document.getElementById(location);
michael@0 241
michael@0 242 let toSelect = document.querySelector(".project-item.selected");
michael@0 243 toSelect = toSelect ? toSelect.id : "";
michael@0 244
michael@0 245 if (toSelect == location) {
michael@0 246 toSelect = null;
michael@0 247 let sibling;
michael@0 248 if (item.previousElementSibling) {
michael@0 249 sibling = item.previousElementSibling;
michael@0 250 } else {
michael@0 251 sibling = item.nextElementSibling;
michael@0 252 }
michael@0 253 if (sibling && !!AppProjects.get(sibling.id)) {
michael@0 254 toSelect = sibling.id;
michael@0 255 }
michael@0 256 }
michael@0 257
michael@0 258 AppProjects.remove(location).then(() => {
michael@0 259 this.selectProject(toSelect);
michael@0 260 });
michael@0 261 },
michael@0 262
michael@0 263 _getProjectManifestURL: function (project) {
michael@0 264 if (project.type == "packaged") {
michael@0 265 return "app://" + project.packagedAppOrigin + "/manifest.webapp";
michael@0 266 } else if (project.type == "hosted") {
michael@0 267 return project.location;
michael@0 268 }
michael@0 269 },
michael@0 270
michael@0 271 install: function(project) {
michael@0 272 if (!this.connected) {
michael@0 273 return promise.reject();
michael@0 274 }
michael@0 275 this.connection.log("Installing the " + project.manifest.name + " app...");
michael@0 276 let installPromise;
michael@0 277 if (project.type == "packaged") {
michael@0 278 installPromise = installPackaged(this.connection.client, this.listTabsResponse.webappsActor, project.location, project.packagedAppOrigin)
michael@0 279 .then(({ appId }) => {
michael@0 280 // If the packaged app specified a custom origin override,
michael@0 281 // we need to update the local project origin
michael@0 282 project.packagedAppOrigin = appId;
michael@0 283 // And ensure the indexed db on disk is also updated
michael@0 284 AppProjects.update(project);
michael@0 285 });
michael@0 286 } else {
michael@0 287 let manifestURLObject = Services.io.newURI(project.location, null, null);
michael@0 288 let origin = Services.io.newURI(manifestURLObject.prePath, null, null);
michael@0 289 let appId = origin.host;
michael@0 290 let metadata = {
michael@0 291 origin: origin.spec,
michael@0 292 manifestURL: project.location
michael@0 293 };
michael@0 294 installPromise = installHosted(this.connection.client, this.listTabsResponse.webappsActor, appId, metadata, project.manifest);
michael@0 295 }
michael@0 296
michael@0 297 installPromise.then(() => {
michael@0 298 this.connection.log("Install completed.");
michael@0 299 }, () => {
michael@0 300 this.connection.log("Install failed.");
michael@0 301 });
michael@0 302
michael@0 303 return installPromise;
michael@0 304 },
michael@0 305
michael@0 306 start: function(project) {
michael@0 307 if (!this.connected) {
michael@0 308 return promise.reject();
michael@0 309 }
michael@0 310 let manifestURL = this._getProjectManifestURL(project);
michael@0 311 return launchApp(this.connection.client,
michael@0 312 this.listTabsResponse.webappsActor,
michael@0 313 manifestURL);
michael@0 314 },
michael@0 315
michael@0 316 stop: function(location) {
michael@0 317 if (!this.connected) {
michael@0 318 return promise.reject();
michael@0 319 }
michael@0 320 let project = AppProjects.get(location);
michael@0 321 let manifestURL = this._getProjectManifestURL(project);
michael@0 322 return closeApp(this.connection.client,
michael@0 323 this.listTabsResponse.webappsActor,
michael@0 324 manifestURL);
michael@0 325 },
michael@0 326
michael@0 327 debug: function(button, location) {
michael@0 328 if (!this.connected) {
michael@0 329 return promise.reject();
michael@0 330 }
michael@0 331 button.disabled = true;
michael@0 332 let project = AppProjects.get(location);
michael@0 333
michael@0 334 let onFailedToStart = (error) => {
michael@0 335 // If not installed, install and open it
michael@0 336 if (error == "NO_SUCH_APP") {
michael@0 337 return this.install(project);
michael@0 338 } else {
michael@0 339 throw error;
michael@0 340 }
michael@0 341 };
michael@0 342 let onStarted = () => {
michael@0 343 // Once we asked the app to launch, the app isn't necessary completely loaded.
michael@0 344 // launch request only ask the app to launch and immediatly returns.
michael@0 345 // We have to keep trying to get app tab actors required to create its target.
michael@0 346 let deferred = promise.defer();
michael@0 347 let loop = (count) => {
michael@0 348 // Ensure not looping for ever
michael@0 349 if (count >= 100) {
michael@0 350 deferred.reject("Unable to connect to the app");
michael@0 351 return;
michael@0 352 }
michael@0 353 // Also, in case the app wasn't installed yet, we also have to keep asking the
michael@0 354 // app to launch, as launch request made right after install may race.
michael@0 355 this.start(project);
michael@0 356 getTargetForApp(
michael@0 357 this.connection.client,
michael@0 358 this.listTabsResponse.webappsActor,
michael@0 359 this._getProjectManifestURL(project)).
michael@0 360 then(deferred.resolve,
michael@0 361 (err) => {
michael@0 362 if (err == "appNotFound")
michael@0 363 setTimeout(loop, 500, count + 1);
michael@0 364 else
michael@0 365 deferred.reject(err);
michael@0 366 });
michael@0 367 };
michael@0 368 loop(0);
michael@0 369 return deferred.promise;
michael@0 370 };
michael@0 371
michael@0 372 // First try to open the app
michael@0 373 this.start(project)
michael@0 374 .then(null, onFailedToStart)
michael@0 375 .then(onStarted)
michael@0 376 .then((target) =>
michael@0 377 top.UI.openAndShowToolboxForTarget(target,
michael@0 378 project.manifest.name,
michael@0 379 project.icon))
michael@0 380 .then(() => {
michael@0 381 // And only when the toolbox is opened, release the button
michael@0 382 button.disabled = false;
michael@0 383 },
michael@0 384 (err) => {
michael@0 385 button.disabled = false;
michael@0 386 let message = err.error ? err.error + ": " + err.message : String(err);
michael@0 387 alert(message);
michael@0 388 this.connection.log(message);
michael@0 389 });
michael@0 390 },
michael@0 391
michael@0 392 reveal: function(location) {
michael@0 393 let project = AppProjects.get(location);
michael@0 394 if (project.type == "packaged") {
michael@0 395 let projectFolder = FileUtils.File(project.location);
michael@0 396 projectFolder.reveal();
michael@0 397 } else {
michael@0 398 // TODO: eventually open hosted apps in firefox
michael@0 399 // when permissions are correctly supported by firefox
michael@0 400 }
michael@0 401 },
michael@0 402
michael@0 403 selectProject: function(location) {
michael@0 404 let projects = AppProjects.store.object.projects;
michael@0 405 let idx = 0;
michael@0 406 for (; idx < projects.length; idx++) {
michael@0 407 if (projects[idx].location == location) {
michael@0 408 break;
michael@0 409 }
michael@0 410 }
michael@0 411
michael@0 412 let oldButton = document.querySelector(".project-item.selected");
michael@0 413 if (oldButton) {
michael@0 414 oldButton.classList.remove("selected");
michael@0 415 }
michael@0 416
michael@0 417 if (idx == projects.length) {
michael@0 418 // Not found. Empty lense.
michael@0 419 let lense = document.querySelector("#lense");
michael@0 420 lense.setAttribute("template-for", '{"path":"","childSelector":""}');
michael@0 421 this.template._processFor(lense);
michael@0 422 return;
michael@0 423 }
michael@0 424
michael@0 425 let button = document.getElementById(location);
michael@0 426 button.classList.add("selected");
michael@0 427
michael@0 428 let template = '{"path":"projects.' + idx + '","childSelector":"#lense-template"}';
michael@0 429
michael@0 430 let lense = document.querySelector("#lense");
michael@0 431 lense.setAttribute("template-for", template);
michael@0 432 this.template._processFor(lense);
michael@0 433
michael@0 434 let project = projects[idx];
michael@0 435 this._showManifestEditor(project).then(() => this.emit("project-selected"));
michael@0 436 },
michael@0 437
michael@0 438 _showManifestEditor: function(project) {
michael@0 439 if (this.manifestEditor) {
michael@0 440 this.manifestEditor.destroy();
michael@0 441 }
michael@0 442 let editorContainer = document.querySelector("#lense .manifest-editor");
michael@0 443 this.manifestEditor = new ManifestEditor(project);
michael@0 444 return this.manifestEditor.show(editorContainer);
michael@0 445 }
michael@0 446 };
michael@0 447
michael@0 448 // This must be bound immediately, as it might be used via the message listener
michael@0 449 // before UI.onload() has been called.
michael@0 450 UI._onConnectionStatusChange = UI._onConnectionStatusChange.bind(UI);
michael@0 451
michael@0 452 EventEmitter.decorate(UI);

mercurial