Add Download button to right click menu

We need a way for users to export data, since it can't be selected and
copied all at once.

video: https://www.youtube.com/watch?v=v7KAKXj41yY&feature=youtu.be
example json: files.tedm.io/devtools_download_example.json

Bug: 711793
Change-Id: I653a1ec6e77b6fdca54a2f958e3c479e9aa89566
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2327241
Commit-Queue: Ted Meyer <[email protected]>
Reviewed-by: Mathias Bynens <[email protected]>
diff --git a/front_end/media/MainView.js b/front_end/media/MainView.js
index 538ed15..db2c787 100644
--- a/front_end/media/MainView.js
+++ b/front_end/media/MainView.js
@@ -61,6 +61,113 @@
 }
 
 /**
+ * @implements TriggerHandler
+ */
+class PlayerDataCollection {
+  constructor() {
+    // Map<Protocol.Media.PlayerProperty>
+    this._properties = {};
+
+    this._messages = [];
+    this._events = [];
+    this._errors = [];
+  }
+
+  /**
+   * @override
+   * @param {!Protocol.Media.PlayerProperty} property
+   */
+  onProperty(property) {
+    this._properties[property.name] = property.value;
+  }
+
+  /**
+   * @override
+   * @param {!Protocol.Media.PlayerError} error */
+  onError(error) {
+    this._errors.push(error);
+  }
+
+  /**
+   * @override
+   * @param {!Protocol.Media.PlayerMessage} message
+   */
+  onMessage(message) {
+    this._messages.push(message);
+  }
+
+  /**
+   * @override
+   * @param {!PlayerEvent} event
+   */
+  onEvent(event) {
+    this._events.push(event);
+  }
+
+  export() {
+    return {'properties': this._properties, 'messages': this._messages, 'events': this._events, 'errors': this._errors};
+  }
+}
+
+/**
+ * @implements TriggerDispatcher
+ */
+class PlayerDataDownloadManager {
+  constructor() {
+    // Map<string, PlayerDataCollection>
+    this._playerDataCollection = new Map();
+  }
+
+  addPlayer(playerID) {
+    this._playerDataCollection.set(playerID, new PlayerDataCollection());
+  }
+
+  /**
+   * @override
+   * @param {string} playerID
+   * @param {!Protocol.Media.PlayerProperty} property
+   */
+  onProperty(playerID, property) {
+    this._playerDataCollection.get(playerID).onProperty(property);
+  }
+
+  /**
+   * @override
+   * @param {string} playerID
+   * @param {!Protocol.Media.PlayerError} error
+   */
+  onError(playerID, error) {
+    this._playerDataCollection.get(playerID).onError(error);
+  }
+
+  /**
+   * @override
+   * @param {string} playerID
+   * @param {!Protocol.Media.PlayerMessage} message
+   */
+  onMessage(playerID, message) {
+    this._playerDataCollection.get(playerID).onMessage(message);
+  }
+
+  /**
+   * @override
+   * @param {string} playerID
+   * @param {!PlayerEvent} event
+   */
+  onEvent(playerID, event) {
+    this._playerDataCollection.get(playerID).onEvent(event);
+  }
+
+  exportPlayerData(playerID) {
+    return this._playerDataCollection.get(playerID).export();
+  }
+
+  deletePlayer(playerID) {
+    this._playerDataCollection.delete(playerID);
+  }
+}
+
+/**
  * @implements {SDK.SDKModel.SDKModelObserver<!Media.MediaModel>}
  * @implements TriggerDispatcher
  */
@@ -75,6 +182,8 @@
     // Map<string>
     this._deletedPlayers = new Set();
 
+    this._downloadStore = new PlayerDataDownloadManager();
+
     this._sidebar = new PlayerListView(this);
     this._sidebar.show(this.panelSidebarElement());
 
@@ -158,6 +267,7 @@
   _onPlayerCreated(playerID) {
     this._sidebar.addMediaElementItem(playerID);
     this._detailPanels.set(playerID, new PlayerDetailView());
+    this._downloadStore.addPlayer(playerID);
   }
 
   /**
@@ -214,6 +324,7 @@
       return;
     }
     this._sidebar.onProperty(playerID, property);
+    this._downloadStore.onProperty(playerID, property);
     this._detailPanels.get(playerID).onProperty(property);
   }
 
@@ -227,6 +338,7 @@
       return;
     }
     this._sidebar.onError(playerID, error);
+    this._downloadStore.onError(playerID, error);
     this._detailPanels.get(playerID).onError(error);
   }
 
@@ -240,6 +352,7 @@
       return;
     }
     this._sidebar.onMessage(playerID, message);
+    this._downloadStore.onMessage(playerID, message);
     this._detailPanels.get(playerID).onMessage(message);
   }
 
@@ -253,6 +366,7 @@
       return;
     }
     this._sidebar.onEvent(playerID, event);
+    this._downloadStore.onEvent(playerID, event);
     this._detailPanels.get(playerID).onEvent(event);
   }
 
@@ -274,5 +388,15 @@
     this._deletedPlayers.add(playerID);
     this._detailPanels.delete(playerID);
     this._sidebar.deletePlayer(playerID);
+    this._downloadStore.deletePlayer(playerID);
+  }
+
+  exportPlayerData(playerID) {
+    const dump = this._downloadStore.exportPlayerData(playerID);
+    const uriContent = 'data:application/octet-stream,' + encodeURIComponent(JSON.stringify(dump, null, 2));
+    const anchor = document.createElement('a');
+    anchor.href = uriContent;
+    anchor.download = playerID + '.json';
+    anchor.click();
   }
 }