Migrate UI to Polymer 3

Change-Id: Icdbfdfce47577f87fa71a9f55fb038f8c0c32216
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/.eslintignore
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..b586e29
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,166 @@
+{
+  "extends": ["eslint:recommended", "google"],
+  "parserOptions": {
+    "ecmaVersion": 8,
+    "sourceType": "module"
+  },
+  "env": {
+    "browser": true,
+    "es6": true
+  },
+  "globals": {
+    "__dirname": false,
+    "app": false,
+    "page": false,
+    "Polymer": false,
+    "process": false,
+    "require": false,
+    "Gerrit": false,
+    "Promise": false,
+    "assert": false,
+    "test": false,
+    "flushAsynchronousOperations": false
+  },
+  "rules": {
+    "arrow-parens": ["error", "as-needed"],
+    "block-spacing": ["error", "always"],
+    "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
+    "camelcase": "off",
+    "comma-dangle": ["error", {
+      "arrays": "always-multiline",
+      "objects": "always-multiline",
+      "imports": "always-multiline",
+      "exports": "always-multiline",
+      "functions": "never"
+    }],
+    "eol-last": "off",
+    "indent": ["error", 2, {
+      "MemberExpression": 2,
+      "FunctionDeclaration": {"body": 1, "parameters": 2},
+      "FunctionExpression": {"body": 1, "parameters": 2},
+      "CallExpression": {"arguments": 2 },
+      "ArrayExpression": 1,
+      "ObjectExpression": 1,
+      "SwitchCase": 1
+    }],
+    "keyword-spacing": ["error", { "after": true, "before": true }],
+    "lines-between-class-members": ["error", "always"],
+    "max-len": [
+      "error",
+      80,
+      2,
+      {
+        "ignoreComments": true,
+        "ignorePattern": "^import .*;$"
+      }
+    ],
+    "new-cap": ["error", { "capIsNewExceptions": ["Polymer", "LegacyElementMixin", "GestureEventListeners", "LegacyDataMixin"] }],
+    "no-console": "off",
+    "no-multiple-empty-lines": [ "error", { "max": 1 } ],
+    "no-prototype-builtins": "off",
+    "no-redeclare": "off",
+    "no-restricted-syntax": [
+      "error",
+      {
+        "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='test'][property.name='only']",
+        "message": "Remove test.only."
+      },
+      {
+        "selector": "ExpressionStatement > CallExpression > MemberExpression[object.name='suite'][property.name='only']",
+        "message": "Remove suite.only."
+      }
+    ],
+    "no-undef": "off",
+    "no-useless-escape": "off",
+    "no-var": "error",
+    "object-shorthand": ["error", "always"],
+    "padding-line-between-statements": [
+      "error",
+      {
+        "blankLine": "always",
+        "prev": "class",
+        "next": "*"
+      },
+      {
+        "blankLine": "always",
+        "prev": "*",
+        "next": "class"
+      }
+    ],
+    "prefer-arrow-callback": "error",
+    "prefer-const": "error",
+    "prefer-spread": "error",
+    "quote-props": ["error", "consistent-as-needed"],
+    "require-jsdoc": "off",
+    "semi": [2, "always"],
+    "template-curly-spacing": "error",
+    "valid-jsdoc": "off",
+    "require-jsdoc": 0,
+    "valid-jsdoc": 0,
+    "jsdoc/check-alignment": 2,
+    "jsdoc/check-examples": 0,
+    "jsdoc/check-indentation": 0,
+    "jsdoc/check-param-names": 0,
+    "jsdoc/check-syntax": 0,
+    "jsdoc/check-tag-names": 0,
+    "jsdoc/check-types": 0,
+    "jsdoc/implements-on-classes": 2,
+    "jsdoc/match-description": 0,
+    "jsdoc/newline-after-description": 2,
+    "jsdoc/no-types": 0,
+    "jsdoc/no-undefined-types": 0,
+    "jsdoc/require-description": 0,
+    "jsdoc/require-description-complete-sentence": 0,
+    "jsdoc/require-example": 0,
+    "jsdoc/require-hyphen-before-param-description": 0,
+    "jsdoc/require-jsdoc": 0,
+    "jsdoc/require-param": 0,
+    "jsdoc/require-param-description": 0,
+    "jsdoc/require-param-name": 2,
+    "jsdoc/require-param-type": 2,
+    "jsdoc/require-returns": 0,
+    "jsdoc/require-returns-check": 0,
+    "jsdoc/require-returns-description": 0,
+    "jsdoc/require-returns-type": 2,
+    "jsdoc/valid-types": 2,
+    "jsdoc/require-file-overview": ["error", {
+      "tags": {
+        "license": {
+          "mustExist": true,
+          "preventDuplicates": true
+        }
+      }
+    }],
+    "import/named": 2,
+    "import/no-unresolved": 2,
+    "import/no-self-import": 2,
+    // The no-cycle rule is slow, because it doesn't cache dependencies.
+    // Disable it.
+    "import/no-cycle": 0,
+    "import/no-useless-path-segments": 2,
+    "import/no-unused-modules": 2,
+    "import/no-default-export": 2
+  },
+  "plugins": [
+    "html",
+    "jsdoc",
+    "import"
+  ],
+  "settings": {
+    "html/report-bad-indent": "error"
+  },
+  "overrides": [
+    {
+      "files": ["*_html.js", "*-styles.js"],
+      "rules": {
+        "max-len": "off"
+      }
+    },
+    {
+      "files": ["*.html"],
+      "rules": {
+        "jsdoc/require-file-overview": "off"
+      }
+    }
+  ]
+}
diff --git a/BUILD b/BUILD
index 7b71db2..e73191c 100644
--- a/BUILD
+++ b/BUILD
@@ -1,3 +1,7 @@
+load("@npm_bazel_rollup//:index.bzl", "rollup_bundle")
+load("//tools/bzl:js.bzl", "polygerrit_plugin")
+load("//tools/bzl:genrule2.bzl", "genrule2")
+load("//tools/js:eslint.bzl", "eslint")
 load("//tools/bzl:plugin.bzl", "gerrit_plugin")
 
 gerrit_plugin(
@@ -9,4 +13,56 @@
         "Gerrit-HttpModule: com.googlesource.gerrit.plugins.imagare.HttpModule",
     ],
     resources = glob(["src/main/**/*"]),
+    resource_jars = [":gr-imagare-static"],
+)
+
+genrule2(
+    name = "gr-imagare-static",
+    srcs = [":gr-imagare"],
+    outs = ["gr-imagare-static.jar"],
+    cmd = " && ".join([
+        "mkdir $$TMP/static",
+        "cp -r $(locations :gr-imagare) $$TMP/static",
+        "cd $$TMP",
+        "zip -Drq $$ROOT/$@ -g .",
+    ]),
+)
+
+rollup_bundle(
+    name = "imagare-bundle",
+    srcs = glob(["gr-imagare/*.js"]),
+    entry_point = "gr-imagare/gr-imagare.js",
+    rollup_bin = "//tools/node_tools:rollup-bin",
+    sourcemap = "hidden",
+    format = 'iife',
+    deps = [
+        "@tools_npm//rollup-plugin-node-resolve",
+    ],
+)
+
+polygerrit_plugin(
+    name = "gr-imagare",
+    app = "imagare-bundle.js",
+    plugin_name = "imagare",
+)
+
+# Define the eslinter for the plugin
+# The eslint macro creates 2 rules: lint_test and lint_bin
+eslint(
+    name = "lint",
+    srcs = glob([
+        "gr-imagare/**/*.js",
+    ]),
+    config = ".eslintrc.json",
+    data = [],
+    extensions = [
+        ".js",
+    ],
+    ignore = ".eslintignore",
+    plugins = [
+        "@npm//eslint-config-google",
+        "@npm//eslint-plugin-html",
+        "@npm//eslint-plugin-import",
+        "@npm//eslint-plugin-jsdoc",
+    ],
 )
diff --git a/gr-imagare/gr-imagare-inline.js b/gr-imagare/gr-imagare-inline.js
new file mode 100644
index 0000000..761d0ef
--- /dev/null
+++ b/gr-imagare/gr-imagare-inline.js
@@ -0,0 +1,207 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+const LINK_DECORATIONS = {
+  NONE: 1,
+  INLINE: 2,
+  TOOLTIP: 3,
+};
+
+class GrImagareInline extends Polymer.GestureEventListeners(
+    Polymer.LegacyElementMixin(
+        Polymer.Element)) {
+  /** @returns {?} template for this component */
+  static get template() { return Polymer.html``; }
+
+  /** @returns {string} name of the component */
+  static get is() { return 'gr-imagare-inline'; }
+
+  /**
+   * Defines properties of the component
+   *
+   * @returns {?}
+   */
+  static get properties() {
+    return {
+      _expandedObserver: MutationObserver,
+      _messageAddedObserver: MutationObserver,
+      _messages: Object,
+      _link_decoration: Number,
+      _pattern: String,
+      _decorator_fn: Function,
+    };
+  }
+
+  attached() {
+    super.attached();
+    this._getAccountPrefs().then(() => {
+      if (this._link_decoration === LINK_DECORATIONS.NONE) {
+        return;
+      }
+
+      this._expandedObserver = new MutationObserver(mutations => {
+        mutations.forEach(mut => {
+          if (!mut.target.classList.contains('expanded')) {
+            return;
+          }
+          const links = this._getLinksFromMessage(mut.target);
+
+          if (!links) {
+            return;
+          }
+
+          for (const link of links) {
+            this._decorator_fn(link);
+          }
+        });
+      });
+
+      this._messageAddedObserver = new MutationObserver(mutations => {
+        mutations.forEach(mut => {
+          mut.addedNodes.forEach(node => {
+            if (node.tagName === 'GR-MESSAGE') {
+              this._addExpandedObservers(node);
+            }
+          });
+        });
+      });
+
+      this._messageAddedObserver.observe(
+          util.querySelector(document.body, 'gr-messages-list'),
+          {
+            childList: true,
+          });
+
+      this._addObserversToMessages();
+    });
+  }
+
+  detached() {
+    super.detached();
+    this._expandedObserver.disconnect();
+    this._messageAddedObserver.disconnect();
+  }
+
+  _addObserversToMessages() {
+    this._messages = this._getMessages();
+
+    if (!this._messages) {
+      return;
+    }
+
+    for (const message of this._messages) {
+      this._addExpandedObservers(message);
+    }
+  }
+
+  _addExpandedObservers(message) {
+    this._expandedObserver.observe(message, {
+      attributes: true,
+      attributeOldValue: true,
+      attributFilter: ['class'],
+    });
+  }
+
+  _getAccountPrefs() {
+    return this.plugin.restApi('/accounts/self/imagare~preference')
+        .get('')
+        .then(prefs => {
+          if (!prefs || !prefs.link_decoration) {
+            this._link_decoration = LINK_DECORATIONS.NONE;
+            this._pattern = '.*';
+          } else {
+            this._link_decoration = LINK_DECORATIONS[
+                prefs.link_decoration.toUpperCase()
+            ];
+            this._pattern = prefs.pattern || '.*';
+          }
+
+          switch (this._link_decoration) {
+            case LINK_DECORATIONS.INLINE:
+              this._decorator_fn = this._insertImage.bind(this);
+              break;
+            case LINK_DECORATIONS.TOOLTIP:
+              this._decorator_fn = this._addTooltip.bind(this);
+              break;
+            case LINK_DECORATIONS.NONE:
+            default:
+              this._decorator_fn = () => {};
+          }
+        });
+  }
+
+  _getMessages() {
+    const messageList = util.querySelector(document.body, 'gr-messages-list');
+    if (messageList) {
+      return util.querySelectorAll(messageList, 'gr-message');
+    }
+  }
+
+  _getLinksFromMessage(message) {
+    let links = [];
+    const linkedTexts = util.querySelectorAll(message, 'gr-linked-text');
+    for (const e of linkedTexts) {
+      const aTags = util.querySelectorAll(e, 'a');
+      if (aTags && aTags.length > 0) {
+        for (const a of aTags) {
+          if (util.querySelectorAll(a, 'img').length > 0) {
+            continue;
+          }
+          if (!a.href.match(this._pattern)) {
+            continue;
+          }
+
+          links = links.concat(a);
+        }
+      }
+    }
+    return links.length > 0 ? links : null;
+  }
+
+  _createImage(url) {
+    const img = document.createElement('img');
+    img.setAttribute('src', url);
+    img.setAttribute('style', 'max-width: 100%; height: auto;');
+
+    return img;
+  }
+
+  _insertImage(link) {
+    if (!link) {
+      return;
+    }
+
+    link.replaceWith(this._createImage(link.href));
+  }
+
+  _addTooltip(link) {
+    if (!link) {
+      return;
+    }
+
+    link.onmouseover = event => {
+      const img = this._createImage(link.href);
+      img.onmouseout = event => {
+        event.target.replaceWith(link);
+      };
+
+      event.target.replaceWith(img);
+    };
+  }
+}
+
+customElements.define(GrImagareInline.is, GrImagareInline);
diff --git a/gr-imagare/gr-imagare-list-item.js b/gr-imagare/gr-imagare-list-item.js
new file mode 100644
index 0000000..1c2f228
--- /dev/null
+++ b/gr-imagare/gr-imagare-list-item.js
@@ -0,0 +1,126 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {htmlTemplate} from './gr-imagare-list-item_html.js';
+
+class GrImagareListItem extends Polymer.GestureEventListeners(
+    Polymer.LegacyElementMixin(
+        Polymer.Element)) {
+  /** @returns {?} template for this component */
+  static get template() { return htmlTemplate; }
+
+  /** @returns {string} name of the component */
+  static get is() { return 'gr-imagare-list-item'; }
+
+  /**
+   * Defines properties of the component
+   *
+   * @returns {?}
+   */
+  static get properties() {
+    return {
+      imageUrl: {
+        type: String,
+        reflectToAttribute: true,
+      },
+      imageName: {
+        type: String,
+        reflectToAttribute: true,
+      },
+      imageData: {
+        type: String,
+        reflectToAttribute: true,
+      },
+      uploaded: {
+        type: Boolean,
+        observer: '_uploadedChanged',
+        reflectToAttribute: true,
+      },
+      _originalImageName: String,
+      _editing: {
+        type: Boolean,
+        value: false,
+      },
+      _imageSrc: String,
+    };
+  }
+
+  attached() {
+    super.attached();
+    this._originalImageName = this.imageName;
+    this._setImage();
+  }
+
+  _handleCancelRenameName() {
+    this.imageName = this._originalImageName;
+    this._editing = false;
+  }
+
+  _handleClearImage() {
+    this.fire('clear');
+  }
+
+  _handleDeleteImage() {
+    this.fire('delete');
+  }
+
+  _handleEditImage() {
+    this._editing = true;
+  }
+
+  _handleSaveName() {
+    this._editing = false;
+
+    if (this._originalImageName === this.imageName) {
+      return;
+    }
+
+    const oldFileType = this._originalImageName.split('.').pop();
+    const newFileType = this.imageName.split('.').pop();
+    if (oldFileType !== newFileType) {
+      this.imageName += `.${oldFileType}`;
+    }
+
+    this.fire(
+        'editName',
+        {oldName: this._originalImageName, newName: this.imageName});
+  }
+
+  _handleUploadImage() {
+    this.fire('upload');
+  }
+
+  _openDeleteDialog() {
+    this.$.deleteOverlay.open();
+  }
+
+  _setImage() {
+    if (this.uploaded) {
+      this.$.thumbnail.setAttribute('src', this.imageUrl);
+    } else {
+      this.$.thumbnail.setAttribute('src', this.imageData);
+    }
+  }
+
+  _uploadedChanged(uploaded) {
+    this.$.uploading.hidden = !uploaded;
+    this.$.staging.hidden = uploaded;
+    this._setImage();
+  }
+}
+
+customElements.define(GrImagareListItem.is, GrImagareListItem);
diff --git a/src/main/resources/static/gr-imagare-list-item.html b/gr-imagare/gr-imagare-list-item_html.js
similarity index 80%
rename from src/main/resources/static/gr-imagare-list-item.html
rename to gr-imagare/gr-imagare-list-item_html.js
index 2f66329..63ae3f2 100644
--- a/src/main/resources/static/gr-imagare-list-item.html
+++ b/gr-imagare/gr-imagare-list-item_html.js
@@ -1,19 +1,21 @@
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-http://www.apache.org/licenses/LICENSE-2.0
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
-<dom-module id="gr-imagare-list-item">
-  <template>
+export const htmlTemplate = Polymer.html`
     <style include="shared-styles"></style>
     <style include="gr-subpage-styles"></style>
     <style>
@@ -130,6 +132,4 @@
         </gr-overlay>
       </div>
     </div>
-  </template>
-  <script src="gr-imagare-list-item.js"></script>
-</dom-module>
+`;
diff --git a/gr-imagare/gr-imagare-pref-menu-item.js b/gr-imagare/gr-imagare-pref-menu-item.js
new file mode 100644
index 0000000..39bf2d7
--- /dev/null
+++ b/gr-imagare/gr-imagare-pref-menu-item.js
@@ -0,0 +1,30 @@
+/**
+ * @license
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {htmlTemplate} from './gr-imagare-pref-menu-item_html.js';
+
+class GrImagarePrefMenuItem extends Polymer.GestureEventListeners(
+    Polymer.LegacyElementMixin(
+        Polymer.Element)) {
+  /** @returns {?} template for this component */
+  static get template() { return htmlTemplate; }
+
+  /** @returns {string} name of the component */
+  static get is() { return 'gr-imagare-pref-menu-item'; }
+}
+
+customElements.define(GrImagarePrefMenuItem.is, GrImagarePrefMenuItem);
diff --git a/gr-imagare/gr-imagare-pref-menu-item_html.js b/gr-imagare/gr-imagare-pref-menu-item_html.js
new file mode 100644
index 0000000..b3a7c6b
--- /dev/null
+++ b/gr-imagare/gr-imagare-pref-menu-item_html.js
@@ -0,0 +1,26 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const htmlTemplate = Polymer.html`
+    <style include="shared-styles"></style>
+    <style include="gr-page-nav-styles"></style>
+    <div class="navStyles">
+      <li>
+        <a href="#ImagarePreferences">Imagare Preferences</a>
+      </li>
+    </div>
+`;
diff --git a/gr-imagare/gr-imagare-preferences.js b/gr-imagare/gr-imagare-preferences.js
new file mode 100644
index 0000000..25ec08d
--- /dev/null
+++ b/gr-imagare/gr-imagare-preferences.js
@@ -0,0 +1,118 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {htmlTemplate} from './gr-imagare-preferences_html.js';
+
+class GrImagarePreferences extends Polymer.GestureEventListeners(
+    Polymer.LegacyElementMixin(
+        Polymer.Element)) {
+  /** @returns {?} template for this component */
+  static get template() { return htmlTemplate; }
+
+  /** @returns {string} name of the component */
+  static get is() { return 'gr-imagare-preferences'; }
+
+  /**
+   * Defines properties of the component
+   *
+   * @returns {?}
+   */
+  static get properties() {
+    return {
+      _defaultImageProject: String,
+      _linkDecoration: String,
+      _stageImages: Boolean,
+      _prefsChanged: {
+        type: Boolean,
+        value: false,
+      },
+      _query: {
+        type: Function,
+        value() {
+          return this._queryProjects.bind(this);
+        },
+      },
+    };
+  }
+
+  attached() {
+    super.attached();
+    this._getUserPreferences();
+  }
+
+  _getUserPreferences() {
+    this.plugin.restApi('/accounts/self/')
+        .get(`imagare~preference`)
+        .then(config => {
+          if (!config) {
+            return;
+          }
+
+          this._linkDecoration = config.link_decoration;
+          this._defaultImageProject = config.default_project;
+          this._stageImages = config.stage;
+        }).catch(response => {
+          this.fire('show-error', {message: response});
+        });
+  }
+
+  _handleImagarePrefsSave() {
+    this.plugin.restApi('/accounts/self/')
+        .put(`imagare~preference`, {
+          default_project: this._defaultImageProject,
+          link_decoration: this._linkDecoration,
+          stage: this._stageImages,
+        }).then(() => {
+          this._prefsChanged = false;
+        }).catch(response => {
+          this.fire('show-error', {message: response});
+        });
+  }
+
+  _handlePrefsChanged() {
+    this._prefsChanged = true;
+  }
+
+  _handleStageImagesChanged(event) {
+    this._handlePrefsChanged();
+    this._stageImages = event.target.checked;
+  }
+
+  _queryProjects(input) {
+    let query;
+    if (!input || input === this._defaultImageProject) {
+      query = '';
+    } else {
+      query = `?prefix=${input}`;
+    }
+
+    return this.plugin.restApi('/a/projects/').get(query)
+        .then(response => {
+          const projects = [];
+          for (const key in response) {
+            if (!response.hasOwnProperty(key)) { continue; }
+            projects.push({
+              name: key,
+              value: decodeURIComponent(response[key].id),
+            });
+          }
+          return projects;
+        });
+  }
+}
+
+customElements.define(GrImagarePreferences.is, GrImagarePreferences);
diff --git a/src/main/resources/static/gr-imagare-preferences.html b/gr-imagare/gr-imagare-preferences_html.js
similarity index 67%
rename from src/main/resources/static/gr-imagare-preferences.html
rename to gr-imagare/gr-imagare-preferences_html.js
index ae26388..4dc8072 100644
--- a/src/main/resources/static/gr-imagare-preferences.html
+++ b/gr-imagare/gr-imagare-preferences_html.js
@@ -1,19 +1,21 @@
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-http://www.apache.org/licenses/LICENSE-2.0
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
-<dom-module id="gr-imagare-preferences">
-  <template>
+export const htmlTemplate = Polymer.html`
     <style include="shared-styles"></style>
     <style include="gr-form-styles"></style>
     <h2 id="ImagarePreferences">Imagare Preferences</h2>
@@ -55,6 +57,4 @@
         Save Changes
       </gr-button>
     </fieldset>
-  </template>
-  <script src="gr-imagare-preferences.js"></script>
-</dom-module>
+`;
diff --git a/gr-imagare/gr-imagare-upload.js b/gr-imagare/gr-imagare-upload.js
new file mode 100644
index 0000000..2a3b125
--- /dev/null
+++ b/gr-imagare/gr-imagare-upload.js
@@ -0,0 +1,393 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './gr-imagare-list-item.js';
+import {htmlTemplate} from './gr-imagare-upload_html.js';
+
+function preventDefaultFn(event) {
+  event.preventDefault();
+}
+
+class GrImagareUpload extends Polymer.GestureEventListeners(
+    Polymer.LegacyElementMixin(
+        Polymer.Element)) {
+  /** @returns {?} template for this component */
+  static get template() { return htmlTemplate; }
+
+  /** @returns {string} name of the component */
+  static get is() { return 'gr-imagare-upload'; }
+
+  /**
+   * Defines properties of the component
+   *
+   * @returns {?}
+   */
+  static get properties() {
+    return {
+      _loading: {
+        type: Boolean,
+        value: true,
+      },
+      _allUploaded: {
+        type: Boolean,
+        value: false,
+      },
+      _imageProject: String,
+      _defaultImageProject: String,
+      _images: {
+        type: Map,
+        value: () => new Map(),
+      },
+      _stageImages: {
+        type: Boolean,
+        value: true,
+      },
+      _undefinedFileCounter: {
+        type: Number,
+        value: 0,
+      },
+      _query: {
+        type: Function,
+        value() {
+          return this._queryProjects.bind(this);
+        },
+      },
+    };
+  }
+
+  static get listeners() {
+    return {
+      clear: '_handleClearImage',
+      delete: '_handleDeleteImage',
+      editName: '_handleEditImageName',
+      upload: '_handleUploadImage',
+    };
+  }
+
+  attached() {
+    super.attached();
+    this.fire('title-change', {title: 'Image Upload'});
+
+    window.addEventListener('dragover', preventDefaultFn, false);
+    window.addEventListener('drop', preventDefaultFn, false);
+    this.$.dragDropArea.addEventListener('paste', preventDefaultFn, false);
+
+    this._getUserPreferences();
+  }
+
+  detached() {
+    window.removeEventListener('dragover', preventDefaultFn, false);
+    window.removeEventListener('drop', preventDefaultFn, false);
+    this.$.dragDropArea.removeEventListener('paste', preventDefaultFn, false);
+  }
+
+  _computeFilenameWithCorrectType(filedata, filename) {
+    const realFiletype = filedata.slice(
+        filedata.indexOf('/') + 1,
+        filedata.indexOf(';'));
+
+    let givenFiletype;
+
+    if (filename.indexOf('.') !== -1) {
+      givenFiletype = filename.split('.').pop();
+    }
+
+    if (!givenFiletype || realFiletype !== givenFiletype) {
+      filename += `.${realFiletype}`;
+    }
+
+    return filename;
+  }
+
+  _computeLoadingClass(loading) {
+    return loading ? 'loading' : '';
+  }
+
+  _computeSettingsUrl() {
+    return `${location.origin}/settings#ImagarePreferences`;
+  }
+
+  _computeUploadAllDisabled() {
+    if (this._images) {
+      for (const value of this._images.values()) {
+        if (!value.uploaded) {
+          this._allUploaded = false;
+          return;
+        }
+      }
+    }
+
+    this._allUploaded = true;
+  }
+
+  _createImageObject(name, data, url, list_entry, uploaded, ref) {
+    return {data, list_entry, name, ref, url, uploaded};
+  }
+
+  _createListEntry(name, data, url) {
+    const imagePanel = document.createElement('gr-imagare-list-item');
+    imagePanel.setAttribute('image-name', name);
+
+    if (data) {
+      imagePanel.setAttribute('image-data', data);
+    }
+
+    if (url) {
+      imagePanel.setAttribute('image-url', url);
+      imagePanel.uploaded = true;
+    } else {
+      imagePanel.uploaded = false;
+    }
+
+    this.$.imageList.appendChild(imagePanel);
+
+    return imagePanel;
+  }
+
+  _deleteImage(image) {
+    this.plugin.restApi('/projects')
+        .delete(`/${this._imageProject}/imagare~images/${image.ref}`)
+        .then(() => {
+          image.list_entry.remove();
+          this._images.delete(image.name);
+          if (!this.$.imageList.hasChildNodes()) {
+            this.$.imageListContainer.hidden = true;
+          }
+        }).catch(response => {
+          this.fire('show-error', {message: response});
+        });
+  }
+
+  _extractImageRef(url) {
+    return url.split('/').slice(-2)[0];
+  }
+
+  _getUserPreferences() {
+    this.plugin.restApi('/accounts/self/')
+        .get(`imagare~preference`)
+        .then(config => {
+          if (!config) {
+            return;
+          }
+
+          this._defaultImageProject = config.default_project;
+          this._imageProject = config.default_project;
+          this._stageImages = config.stage;
+          this._loading = false;
+        });
+  }
+
+  _handleClearAllImages() {
+    while (this.$.imageList.firstChild) {
+      this.$.imageList.removeChild(this.$.imageList.firstChild);
+    }
+    this.$.imageListContainer.hidden = true;
+
+    this._images.clear();
+  }
+
+  _handleClearImage(event) {
+    event.stopPropagation();
+    this._images.delete(event.target.imageName);
+    event.target.remove();
+    if (!this.$.imageList.hasChildNodes()) {
+      this.$.imageListContainer.hidden = true;
+    }
+  }
+
+  _handleDeleteImage(event) {
+    event.stopPropagation();
+    this._deleteImage(this._images.get(event.target.imageName));
+  }
+
+  _handleDrop(event) {
+    event.preventDefault();
+    event.stopPropagation();
+
+    for (const file of event.dataTransfer.files) {
+      if (!file.type.match('image/.*')) {
+        this.fire('show-error', {message: `No image file: ${file.name}`});
+      }
+      const fr = new FileReader();
+      fr.file = file;
+      fr.onload = fileLoadEvent => this._handleFileLoadEvent(
+          fr.file.name, fileLoadEvent);
+      fr.readAsDataURL(file);
+    }
+  }
+
+  _handleEditImageName(event) {
+    event.stopPropagation();
+    const editedImage = this._images.get(event.detail.oldName);
+    if (this._images.has(event.detail.newName)) {
+      this.fire(
+          'show-error',
+          {message: 'An image with the same name was already staged.'});
+      editedImage.list_entry.setAttribute('image-name', event.detail.oldName);
+    } else {
+      editedImage.name = event.detail.newName;
+      this._images.set(editedImage.name, editedImage);
+      this._images.delete(event.detail.oldName);
+    }
+  }
+
+  _handleFileLoadEvent(filename, event) {
+    const correctedFilename = this._computeFilenameWithCorrectType(
+        event.target.result, filename);
+    if (this._stageImages) {
+      this._stageImage(correctedFilename, event.target.result);
+    } else {
+      const image = this._createImageObject(
+          correctedFilename, event.target.result);
+      this._images.set(correctedFilename, image);
+      this._uploadImage(image);
+    }
+  }
+
+  _handleKeyPress(event) {
+    const ctrlDown = event.ctrlKey || event.metaKey;
+    if (!ctrlDown) {
+      event.preventDefault();
+      event.stopPropagation();
+    }
+  }
+
+  _handleImagePathChanged(event) {
+    for (const file of event.target.files) {
+      const fr = new FileReader();
+      fr.file = file;
+      fr.onload = fileLoadEvent => this._handleFileLoadEvent(
+          fr.file.name, fileLoadEvent);
+      fr.readAsDataURL(file);
+    }
+
+    event.target.value = '';
+  }
+
+  _handlePaste(event) {
+    const clipboardData = event.clipboardData
+        || event.originalEvent.clipboardData;
+    const items = clipboardData.items;
+    if (JSON.stringify(items)) {
+      let blob;
+      for (const item of items) {
+        if (item.type.indexOf('image') === 0) {
+          blob = item.getAsFile();
+        }
+      }
+      if (blob) {
+        const fr = new FileReader();
+        fr.onload = fileLoadEvent => {
+          const filename = `undefined-${this._undefinedFileCounter}`;
+          this._undefinedFileCounter++;
+          this._handleFileLoadEvent(filename, fileLoadEvent);
+        };
+        fr.readAsDataURL(blob);
+      } else {
+        event.preventDefault();
+        this.fire('show-error', {message: `No image file`});
+      }
+    }
+  }
+
+  _handleUploadAllImages() {
+    for (const image of this._images.values()) {
+      this._uploadImage(image);
+    }
+  }
+
+  _handleUploadImage(event) {
+    event.stopPropagation();
+    const image = this._createImageObject(
+        event.target.imageName,
+        event.target.imageData,
+        null,
+        event.target);
+    this._images.set(image.name, image);
+    this._uploadImage(image);
+  }
+
+  _queryProjects(input) {
+    let query;
+    if (!input || input === this._defaultImageProject) {
+      query = '';
+    } else {
+      query = `?prefix=${input}`;
+    }
+
+    return this.plugin.restApi('/a/projects/').get(query)
+        .then(response => {
+          const projects = [];
+          for (const key in response) {
+            if (!response.hasOwnProperty(key)) { continue; }
+            projects.push({
+              name: key,
+              value: decodeURIComponent(response[key].id),
+            });
+          }
+          return projects;
+        });
+  }
+
+  _stageImage(name, data) {
+    if (this._images.has(name)) {
+      const fileName = name.slice(0, name.lastIndexOf('.'));
+      const fileExtension = name.slice(name.lastIndexOf('.'));
+      name = `${fileName}-${this._undefinedFileCounter}${fileExtension}`;
+      this._undefinedFileCounter++;
+    }
+    const imagePanel = this._createListEntry(name, data, null);
+    this._images.set(
+        name, this._createImageObject(name, data, null, imagePanel));
+    this.$.imageListContainer.hidden = false;
+    this._computeUploadAllDisabled();
+  }
+
+  _uploadImage(image) {
+    if (image && image.uploaded) {
+      return;
+    }
+
+    this.plugin.restApi('/projects')
+        .post(`/${this._imageProject}/imagare~images`, {
+          image_data: image.data,
+          file_name: image.name,
+        })
+        .then(response => {
+          if (!image.list_entry) {
+            image.list_entry = this._createListEntry(
+                image.name, image.data, response.url);
+          } else {
+            image.list_entry.setAttribute('image-url', response.url);
+            image.list_entry.uploaded = true;
+          }
+
+          this._images.set(
+              image.name,
+              this._createImageObject(
+                  image.name, image.data, response.url, image.list_entry, true,
+                  this._extractImageRef(response.url)));
+
+          this.$.imageListContainer.hidden = false;
+          this._computeUploadAllDisabled();
+        }).catch(response => {
+          this.fire('show-error', {message: response});
+        });
+  }
+}
+
+customElements.define(GrImagareUpload.is, GrImagareUpload);
diff --git a/src/main/resources/static/gr-imagare-upload.html b/gr-imagare/gr-imagare-upload_html.js
similarity index 77%
rename from src/main/resources/static/gr-imagare-upload.html
rename to gr-imagare/gr-imagare-upload_html.js
index 814ade2..cf84eee 100644
--- a/src/main/resources/static/gr-imagare-upload.html
+++ b/gr-imagare/gr-imagare-upload_html.js
@@ -1,22 +1,21 @@
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-http://www.apache.org/licenses/LICENSE-2.0
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 
-<link rel="import"
-      href="./gr-imagare-list-item.html">
-
-<dom-module id="gr-imagare-upload">
-  <template>
+export const htmlTemplate = Polymer.html`
     <style include="shared-styles"></style>
     <style include="gr-subpage-styles"></style>
     <style include="gr-form-styles"></style>
@@ -70,8 +69,8 @@
           <fieldset>
             <h2>Settings</h2>
             <section>
-              The user preferences for the image upload can be changed
-              <a href="[[_computeSettingsUrl()]]">here</a>.
+              The preferences for the image upload can be changed
+              in the <a href="[[_computeSettingsUrl()]]">user settings</a>.
             </section>
             <section>
               <span class="title">Project</span>
@@ -127,6 +126,4 @@
         </div>
       </main>
     </div>
-  </template>
-  <script src="gr-imagare-upload.js"></script>
-</dom-module>
+`;
diff --git a/gr-imagare/gr-imagare.js b/gr-imagare/gr-imagare.js
new file mode 100644
index 0000000..e6094b7
--- /dev/null
+++ b/gr-imagare/gr-imagare.js
@@ -0,0 +1,35 @@
+/**
+ * @license
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import './gr-imagare-inline.js';
+import './gr-imagare-preferences.js';
+import './gr-imagare-pref-menu-item.js';
+import './gr-imagare-upload.js';
+
+Gerrit.install(plugin => {
+  plugin.restApi('/config/server/').get('imagare~config').then(config => {
+    if (config && config.enable_image_server) {
+      plugin.screen('upload', 'gr-imagare-upload');
+    }
+  });
+  plugin.registerCustomComponent(
+      'change-view-integration', 'gr-imagare-inline');
+  plugin.registerCustomComponent(
+      'settings-screen', 'gr-imagare-preferences');
+  plugin.registerCustomComponent(
+      'settings-menu-item', 'gr-imagare-pref-menu-item');
+});
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..eda0784
--- /dev/null
+++ b/package.json
@@ -0,0 +1,13 @@
+{
+  "name": "imagare",
+  "description": "Imagare plugin",
+  "browser": true,
+  "scripts": {
+    "safe_bazelisk": "if which bazelisk >/dev/null; then bazel_bin=bazelisk; else bazel_bin=bazel; fi && $bazel_bin",
+    "eslint": "npm run safe_bazelisk test :lint_test",
+    "eslintfix": "npm run safe_bazelisk run :lint_bin -- -- --fix $(pwd)"
+  },
+  "devDependencies": {},
+  "license": "Apache-2.0",
+  "private": true
+}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java b/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java
index f7960e8..634a7fe 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/imagare/HttpModule.java
@@ -39,6 +39,6 @@
       serveRegex("^" + ImageServlet.PATH_PREFIX + "(.+)?$").with(ImageServlet.class);
     }
 
-    DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(new JavaScriptPlugin("imagare.html"));
+    DynamicSet.bind(binder(), WebUiPlugin.class).toInstance(new JavaScriptPlugin("imagare.js"));
   }
 }
diff --git a/src/main/resources/static/gr-imagare-inline.html b/src/main/resources/static/gr-imagare-inline.html
deleted file mode 100644
index 9b698fa..0000000
--- a/src/main/resources/static/gr-imagare-inline.html
+++ /dev/null
@@ -1,19 +0,0 @@
- <!--
-@license
-Copyright (C) 2019 The Android Open Source Project
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-http://www.apache.org/licenses/LICENSE-2.0
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<dom-module id="gr-imagare-inline">
-  <template>
-  </template>
-  <script src="gr-imagare-inline.js"></script>
-</dom-module>
diff --git a/src/main/resources/static/gr-imagare-inline.js b/src/main/resources/static/gr-imagare-inline.js
deleted file mode 100644
index 903b53e..0000000
--- a/src/main/resources/static/gr-imagare-inline.js
+++ /dev/null
@@ -1,192 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-(function () {
-  'use strict';
-
-  const LINK_DECORATIONS = {
-    NONE: 1,
-    INLINE: 2,
-    TOOLTIP: 3,
-  };
-
-  Polymer({
-    is: 'gr-imagare-inline',
-
-    properties: {
-      _expandedObserver: MutationObserver,
-      _messageAddedObserver: MutationObserver,
-      _messages: Object,
-      _link_decoration: Number,
-      _pattern: String,
-      _decorator_fn: Function,
-    },
-
-    attached() {
-      this._getAccountPrefs().then(() => {
-        if (this._link_decoration === LINK_DECORATIONS.NONE) {
-          return;
-        }
-
-        this._expandedObserver = new MutationObserver(mutations => {
-          mutations.forEach(mut => {
-            if (!mut.target.classList.contains('expanded')){
-              return;
-            }
-            let links = this._getLinksFromMessage(mut.target);
-
-            if (!links) {
-              return;
-            }
-
-            for (const link of links) {
-              this._decorator_fn(link);
-            }
-          });
-        });
-
-        this._messageAddedObserver = new MutationObserver(mutations => {
-          mutations.forEach(mut => {
-            mut.addedNodes.forEach(node => {
-              if (node.tagName === "GR-MESSAGE") {
-                this._addExpandedObservers(node);
-              }
-            });
-          });
-        });
-
-        this._messageAddedObserver.observe(
-          // TODO(Thomas): The util methods were removed in change 270988, which
-          // will break this code in newer Gerrit versions (3.3+). At this point
-          // the plugin should implement the querySelector(All)-methods itself.
-          util.querySelector(document.body, 'gr-messages-list'),
-          {
-            childList: true,
-          });
-
-        this._addObserversToMessages();
-      });
-    },
-
-    detached() {
-      this._expandedObserver.disconnect();
-      this._messageAddedObserver.disconnect();
-    },
-
-    _addObserversToMessages() {
-      this._messages = this._getMessages();
-
-      if (!this._messages) {
-        return;
-      }
-
-      for (const message of this._messages) {
-        this._addExpandedObservers(message);
-      }
-    },
-
-    _addExpandedObservers(message) {
-      this._expandedObserver.observe(message, {
-        attributes: true,
-        attributeOldValue: true,
-        attributFilter: ['class'],
-      });
-    },
-
-    _getAccountPrefs() {
-      return this.plugin.restApi('/accounts/self/imagare~preference')
-        .get('')
-        .then(prefs => {
-          if (!prefs || !prefs.link_decoration) {
-            this._link_decoration = LINK_DECORATIONS.NONE;
-            this._pattern = '.*';
-          } else {
-            this._link_decoration = LINK_DECORATIONS[prefs.link_decoration.toUpperCase()];
-            this._pattern = prefs.pattern || '.*';
-          }
-
-          switch (this._link_decoration) {
-            case LINK_DECORATIONS.INLINE:
-              this._decorator_fn = this._insertImage.bind(this);
-              break;
-            case LINK_DECORATIONS.TOOLTIP:
-              this._decorator_fn = this._addTooltip.bind(this);
-              break;
-            case LINK_DECORATIONS.NONE:
-            default:
-              this._decorator_fn = () => {};
-          }
-        });
-    },
-
-    _getMessages() {
-      let messageList = util.querySelector(document.body, 'gr-messages-list');
-      if (messageList) {
-        return util.querySelectorAll(messageList, 'gr-message');
-      }
-    },
-
-    _getLinksFromMessage(message) {
-      let links = [];
-      let linkedTexts = util.querySelectorAll(message, 'gr-linked-text');
-      for (const e of linkedTexts) {
-        let aTags = util.querySelectorAll(e, 'a');
-        if (aTags && aTags.length > 0){
-          for (const a of aTags){
-            if (util.querySelectorAll(a, 'img').length > 0) {
-              continue;
-            }
-            if (!a.href.match(this._pattern)) {
-              continue;
-            }
-
-            links = links.concat(a);
-          }
-        }
-      }
-      return links.length > 0 ? links : null;
-    },
-
-    _createImage(url) {
-      let img = document.createElement('img');
-      img.setAttribute("src", url);
-      img.setAttribute("style", "max-width: 100%; height: auto;");
-
-      return img;
-    },
-
-    _insertImage(link) {
-      if (!link) {
-        return;
-      }
-
-      link.replaceWith(this._createImage(link.href));
-    },
-
-    _addTooltip(link) {
-      if (!link) {
-        return;
-      }
-
-      link.onmouseover = (event) => {
-        let img = this._createImage(link.href);
-        img.onmouseout = (event) => {
-          event.target.replaceWith(link);
-        }
-
-        event.target.replaceWith(img);
-      }
-    },
-  });
-})();
diff --git a/src/main/resources/static/gr-imagare-list-item.js b/src/main/resources/static/gr-imagare-list-item.js
deleted file mode 100644
index 5f1cffe..0000000
--- a/src/main/resources/static/gr-imagare-list-item.js
+++ /dev/null
@@ -1,107 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the 'License');
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an 'AS IS' BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-(function () {
-  'use strict';
-
-  Polymer({
-    is: 'gr-imagare-list-item',
-
-    properties: {
-      imageUrl: {
-        type: String,
-        reflectToAttribute: true,
-      },
-      imageName: {
-        type: String,
-        reflectToAttribute: true,
-      },
-      imageData: {
-        type: String,
-        reflectToAttribute: true,
-      },
-      uploaded: {
-        type: Boolean,
-        observer: '_uploadedChanged',
-        reflectToAttribute: true,
-      },
-      _originalImageName: String,
-      _editing: {
-        type: Boolean,
-        value: false,
-      },
-      _imageSrc: String,
-    },
-
-    attached() {
-      this._originalImageName = this.imageName;
-      this._setImage();
-    },
-
-    _handleCancelRenameName() {
-      this.imageName = this._originalImageName;
-      this._editing = false;
-    },
-
-    _handleClearImage() {
-      this.fire("clear");
-    },
-
-    _handleDeleteImage() {
-      this.fire("delete");
-    },
-
-    _handleEditImage() {
-      this._editing = true;
-    },
-
-    _handleSaveName() {
-      this._editing = false;
-
-      if (this._originalImageName === this.imageName) {
-        return;
-      }
-
-      let oldFileType = this._originalImageName.split('.').pop();
-      let newFileType = this.imageName.split('.').pop();
-      if (oldFileType !== newFileType) {
-        this.imageName += `.${oldFileType}`;
-      }
-
-      this.fire("editName", {oldName: this._originalImageName, newName: this.imageName});
-    },
-
-    _handleUploadImage() {
-      this.fire("upload");
-    },
-
-    _openDeleteDialog() {
-      this.$.deleteOverlay.open();
-    },
-
-    _setImage() {
-      if (this.uploaded) {
-        this.$.thumbnail.setAttribute('src', this.imageUrl);
-      } else {
-        this.$.thumbnail.setAttribute('src', this.imageData);
-      }
-    },
-
-    _uploadedChanged(uploaded) {
-      this.$.uploading.hidden = !uploaded;
-      this.$.staging.hidden = uploaded;
-      this._setImage();
-    },
-  });
-})();
diff --git a/src/main/resources/static/gr-imagare-pref-menu-item.html b/src/main/resources/static/gr-imagare-pref-menu-item.html
deleted file mode 100644
index ee8b989..0000000
--- a/src/main/resources/static/gr-imagare-pref-menu-item.html
+++ /dev/null
@@ -1,29 +0,0 @@
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-http://www.apache.org/licenses/LICENSE-2.0
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<dom-module id="gr-imagare-pref-menu-item">
-  <template>
-    <style include="shared-styles"></style>
-    <style include="gr-page-nav-styles">
-      li {
-        padding-left: 1.5em;
-        padding-right: 1.5em;
-      }
-    </style>
-    <li class="navStyles">
-      <a href="#ImagarePreferences">Imagare Preferences</a>
-    </li>
-  </template>
-  <script src="gr-imagare-pref-menu-item.js"></script>
-</dom-module>
diff --git a/src/main/resources/static/gr-imagare-pref-menu-item.js b/src/main/resources/static/gr-imagare-pref-menu-item.js
deleted file mode 100644
index 3e1a7b0..0000000
--- a/src/main/resources/static/gr-imagare-pref-menu-item.js
+++ /dev/null
@@ -1,21 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the 'License');
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an 'AS IS' BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-(function () {
-  'use strict';
-
-  Polymer({
-    is: 'gr-imagare-pref-menu-item',
-  });
-})();
diff --git a/src/main/resources/static/gr-imagare-preferences.js b/src/main/resources/static/gr-imagare-preferences.js
deleted file mode 100644
index d1f868f..0000000
--- a/src/main/resources/static/gr-imagare-preferences.js
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the 'License');
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an 'AS IS' BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-(function () {
-  'use strict';
-
-  Polymer({
-    is: 'gr-imagare-preferences',
-
-    properties: {
-      _defaultImageProject: String,
-      _linkDecoration: String,
-      _stageImages: Boolean,
-      _prefsChanged: {
-        type: Boolean,
-        value: false
-      },
-      _query: {
-        type: Function,
-        value() {
-          return this._queryProjects.bind(this);
-        },
-      },
-    },
-
-    attached() {
-      this._getUserPreferences();
-    },
-
-    _getUserPreferences() {
-      this.plugin.restApi('/accounts/self/')
-        .get(`imagare~preference`)
-        .then(config => {
-          if (!config) {
-            return;
-          }
-
-          this._linkDecoration = config.link_decoration;
-          this._defaultImageProject = config.default_project;
-          this._stageImages = config.stage;
-        }).catch(response => {
-          this.fire('show-error', {message: response});
-        });
-    },
-
-    _handleImagarePrefsSave(){
-      this.plugin.restApi('/accounts/self/')
-        .put(`imagare~preference`, {
-          default_project: this._defaultImageProject,
-          link_decoration: this._linkDecoration,
-          stage: this._stageImages,
-        }).then(() => {
-          this._prefsChanged = false;
-        }).catch(response => {
-          this.fire('show-error', {message: response});
-        });
-    },
-
-    _handlePrefsChanged() {
-      this._prefsChanged = true;
-    },
-
-    _handleStageImagesChanged(event){
-      this._handlePrefsChanged();
-      this._stageImages = event.target.checked;
-    },
-
-    _queryProjects(input) {
-      let query;
-      if (!input || input === this._defaultImageProject) {
-        query = '';
-      } else {
-        query = `?prefix=${input}`;
-      }
-
-      return this.plugin.restApi('/a/projects/').get(query)
-          .then(response => {
-            const projects = [];
-            for (const key in response) {
-              if (!response.hasOwnProperty(key)) { continue; }
-              projects.push({
-                name: key,
-                value: decodeURIComponent(response[key].id),
-              });
-            }
-            return projects;
-          });
-    },
-  });
-})();
diff --git a/src/main/resources/static/gr-imagare-upload.js b/src/main/resources/static/gr-imagare-upload.js
deleted file mode 100644
index 40150c1..0000000
--- a/src/main/resources/static/gr-imagare-upload.js
+++ /dev/null
@@ -1,374 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the 'License');
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an 'AS IS' BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-(function () {
-  'use strict';
-
-  function preventDefaultFn(event) {
-    event.preventDefault();
-  }
-
-  Polymer({
-    is: 'gr-imagare-upload',
-
-    properties: {
-      _loading: {
-        type: Boolean,
-        value: true,
-      },
-      _allUploaded: {
-        type: Boolean,
-        value: false,
-      },
-      _imageProject: String,
-      _defaultImageProject: String,
-      _images: {
-        type: Map,
-        value: () => new Map(),
-      },
-      _stageImages: {
-        type: Boolean,
-        value: true,
-      },
-      _undefinedFileCounter: {
-        type: Number,
-        value: 0,
-      },
-      _query: {
-        type: Function,
-        value() {
-          return this._queryProjects.bind(this);
-        },
-      },
-    },
-
-    listeners: {
-      clear: '_handleClearImage',
-      delete: '_handleDeleteImage',
-      editName: '_handleEditImageName',
-      upload: '_handleUploadImage',
-    },
-
-    attached() {
-      this.fire('title-change', { title: 'Image Upload' });
-
-      window.addEventListener('dragover', preventDefaultFn, false);
-      window.addEventListener('drop', preventDefaultFn, false);
-      this.$.dragDropArea.addEventListener('paste', preventDefaultFn, false);
-
-      this._getUserPreferences();
-    },
-
-    detached() {
-      window.removeEventListener('dragover', preventDefaultFn, false);
-      window.removeEventListener('drop', preventDefaultFn, false);
-      this.$.dragDropArea.removeEventListener('paste', preventDefaultFn, false);
-    },
-
-    _computeFilenameWithCorrectType(filedata, filename) {
-      let realFiletype = filedata.slice(
-        filedata.indexOf('/') + 1,
-        filedata.indexOf(';'));
-
-      let givenFiletype;
-
-      if (filename.indexOf(".") !== -1) {
-        givenFiletype = filename.split('.').pop();
-      }
-
-      if (!givenFiletype || realFiletype !== givenFiletype) {
-        filename += `.${realFiletype}`;
-      }
-
-      return filename;
-    },
-
-    _computeLoadingClass(loading) {
-      return loading ? 'loading' : '';
-    },
-
-    _computeSettingsUrl() {
-      return `${location.origin}/settings#ImagarePreferences`;
-    },
-
-    _computeUploadAllDisabled() {
-      if (this._images) {
-        for (let value of this._images.values()) {
-          if (!value.uploaded) {
-            this._allUploaded = false;
-            return;
-          }
-        }
-      }
-
-      this._allUploaded = true;
-    },
-
-    _createImageObject(name, data, url, list_entry, uploaded, ref) {
-      return {
-        data: data,
-        list_entry: list_entry,
-        name: name,
-        ref: ref,
-        url: url,
-        uploaded: uploaded,
-      }
-    },
-
-    _createListEntry(name, data, url) {
-      let imagePanel = document.createElement('gr-imagare-list-item');
-      imagePanel.setAttribute("image-name", name);
-
-      if (data) {
-        imagePanel.setAttribute("image-data", data);
-      }
-
-      if (url) {
-        imagePanel.setAttribute("image-url", url);
-        imagePanel.uploaded = true;
-      } else {
-        imagePanel.uploaded = false;
-      }
-
-      this.$.imageList.appendChild(imagePanel);
-
-      return imagePanel;
-    },
-
-    _deleteImage(image) {
-      this.plugin.restApi('/projects')
-        .delete(`/${this._imageProject}/imagare~images/${image.ref}`)
-        .then(() => {
-          image.list_entry.remove();
-          this._images.delete(image.name);
-          if (!this.$.imageList.hasChildNodes()) {
-            this.$.imageListContainer.hidden = true;
-          }
-        }).catch(response => {
-          this.fire('show-error', { message: response });
-        });
-    },
-
-    _extractImageRef(url) {
-      return url.split('/').slice(-2)[0];
-    },
-
-    _getUserPreferences() {
-      this.plugin.restApi('/accounts/self/')
-        .get(`imagare~preference`)
-        .then(config => {
-          if (!config) {
-            return;
-          }
-
-          this._defaultImageProject = config.default_project;
-          this._imageProject = config.default_project;
-          this._stageImages = config.stage;
-          this._loading = false;
-        });
-    },
-
-    _handleClearAllImages() {
-      while (this.$.imageList.firstChild) {
-        this.$.imageList.removeChild(this.$.imageList.firstChild);
-      }
-      this.$.imageListContainer.hidden = true;
-
-      this._images.clear()
-    },
-
-    _handleClearImage(event) {
-      event.stopPropagation();
-      this._images.delete(event.target.imageName);
-      event.target.remove();
-      if (!this.$.imageList.hasChildNodes()) {
-        this.$.imageListContainer.hidden = true;
-      }
-    },
-
-    _handleDeleteImage(event) {
-      event.stopPropagation();
-      this._deleteImage(this._images.get(event.target.imageName));
-    },
-
-    _handleDrop(event) {
-      event.preventDefault();
-      event.stopPropagation();
-
-      for (let file of event.dataTransfer.files) {
-        if (!file.type.match('image/.*')) {
-          this.fire('show-error', { message: `No image file: ${file.name}` });
-        }
-        let fr = new FileReader();
-        fr.file = file;
-        fr.onload = fileLoadEvent => this._handleFileLoadEvent(
-          fr.file.name, fileLoadEvent);
-        fr.readAsDataURL(file);
-      }
-    },
-
-    _handleEditImageName(event) {
-      event.stopPropagation();
-      let editedImage = this._images.get(event.detail.oldName);
-      if (this._images.has(event.detail.newName)) {
-        this.fire('show-error', { message: 'An image with the same name was already staged.' });
-        editedImage.list_entry.setAttribute("image-name", event.detail.oldName);
-      } else {
-        editedImage.name = event.detail.newName;
-        this._images.set(editedImage.name, editedImage);
-        this._images.delete(event.detail.oldName);
-      }
-    },
-
-    _handleFileLoadEvent(filename, event) {
-      let correctedFilename = this._computeFilenameWithCorrectType(
-        event.target.result, filename);
-      if (this._stageImages) {
-        this._stageImage(correctedFilename, event.target.result);
-      } else {
-        let image = this._createImageObject(correctedFilename, event.target.result);
-        this._images.set(correctedFilename, image);
-        this._uploadImage(image);
-      }
-    },
-
-    _handleKeyPress(event) {
-      let ctrlDown = event.ctrlKey || event.metaKey;
-      if (!ctrlDown) {
-        event.preventDefault();
-        event.stopPropagation();
-      }
-    },
-
-    _handleImagePathChanged(event) {
-      for (let file of event.target.files) {
-        let fr = new FileReader();
-        fr.file = file;
-        fr.onload = fileLoadEvent => this._handleFileLoadEvent(
-          fr.file.name, fileLoadEvent);
-        fr.readAsDataURL(file);
-      }
-
-      event.target.value = '';
-    },
-
-    _handlePaste(event) {
-      let clipboardData = event.clipboardData || event.originalEvent.clipboardData;
-      let items = clipboardData.items;
-      if (JSON.stringify(items)) {
-        let blob;
-        for (let item of items) {
-          if (item.type.indexOf("image") === 0) {
-            blob = item.getAsFile();
-          }
-        }
-        if (blob) {
-          let fr = new FileReader();
-          fr.onload = fileLoadEvent => {
-            let filename = `undefined-${this._undefinedFileCounter}`;
-            this._undefinedFileCounter++;
-            this._handleFileLoadEvent(filename, fileLoadEvent);
-          };
-          fr.readAsDataURL(blob);
-        } else {
-          event.preventDefault();
-          this.fire('show-error', { message: `No image file` });
-        }
-      }
-    },
-
-    _handleUploadAllImages() {
-      for (let image of this._images.values()) {
-        this._uploadImage(image);
-      }
-    },
-
-    _handleUploadImage(event) {
-      event.stopPropagation();
-      let image = this._createImageObject(
-        event.target.imageName,
-        event.target.imageData,
-        null,
-        event.target);
-      this._images.set(image.name, image);
-      this._uploadImage(image);
-    },
-
-    _queryProjects(input) {
-      let query;
-      if (!input || input === this._defaultImageProject) {
-        query = '';
-      } else {
-        query = `?prefix=${input}`;
-      }
-
-      return this.plugin.restApi('/a/projects/').get(query)
-        .then(response => {
-          const projects = [];
-          for (const key in response) {
-            if (!response.hasOwnProperty(key)) { continue; }
-            projects.push({
-              name: key,
-              value: decodeURIComponent(response[key].id),
-            });
-          }
-          return projects;
-        });
-    },
-
-    _stageImage(name, data) {
-      if (this._images.has(name)) {
-        let fileName = name.slice(0, name.lastIndexOf('.'));
-        let fileExtension = name.slice(name.lastIndexOf('.'));
-        name = `${fileName}-${this._undefinedFileCounter}${fileExtension}`;
-        this._undefinedFileCounter++;
-      }
-      let imagePanel = this._createListEntry(name, data, null);
-      this._images.set(name, this._createImageObject(name, data, null, imagePanel));
-      this.$.imageListContainer.hidden = false;
-      this._computeUploadAllDisabled();
-    },
-
-    _uploadImage(image) {
-      if (image && image.uploaded) {
-        return;
-      }
-
-      this.plugin.restApi('/projects')
-        .post(`/${this._imageProject}/imagare~images`, {
-          image_data: image.data,
-          file_name: image.name,
-        })
-        .then(response => {
-          if (!image.list_entry) {
-            image.list_entry = this._createListEntry(image.name, image.data, response.url);
-          } else {
-            image.list_entry.setAttribute("image-url", response.url);
-            image.list_entry.uploaded = true;
-          }
-
-          this._images.set(
-            image.name,
-            this._createImageObject(
-              image.name, image.data, response.url, image.list_entry, true,
-              this._extractImageRef(response.url)));
-
-          this.$.imageListContainer.hidden = false;
-          this._computeUploadAllDisabled();
-        }).catch(response => {
-          this.fire('show-error', { message: response });
-        });
-    }
-  });
-})();
diff --git a/src/main/resources/static/imagare.html b/src/main/resources/static/imagare.html
deleted file mode 100644
index 7e3ee3b..0000000
--- a/src/main/resources/static/imagare.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<!--
-@license
-Copyright (C) 2019 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<link rel="import" href="./gr-imagare-inline.html">
-<link rel="import" href="./gr-imagare-preferences.html">
-<link rel="import" href="./gr-imagare-pref-menu-item.html">
-<link rel="import" href="./gr-imagare-upload.html">
-
-<dom-module id="imagare">
-  <script>
-    Gerrit.install(plugin => {
-      plugin.restApi('/config/server/').get('imagare~config').then(config => {
-        if (config && config.enable_image_server) {
-          plugin.screen('upload', 'gr-imagare-upload');
-        }
-      });
-      plugin.registerCustomComponent('change-view-integration', 'gr-imagare-inline');
-      plugin.registerCustomComponent('settings-screen', 'gr-imagare-preferences');
-      plugin.registerCustomComponent('settings-menu-item', 'gr-imagare-pref-menu-item');
-    });
-  </script>
-</dom-module>