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.

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

mercurial