chrome-devtools-frontend
Version:
Chrome DevTools UI
191 lines (162 loc) • 7.47 kB
text/typescript
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as i18n from '../../core/i18n/i18n.js';
import * as UI from '../../ui/legacy/legacy.js';
import playerListViewStyles from './playerListView.css.js';
import type * as Protocol from '../../generated/protocol.js';
import type {MainView, TriggerDispatcher} from './MainView.js';
import type {PlayerEvent} from './MediaModel.js';
import {PlayerPropertyKeys} from './PlayerPropertiesView.js';
const UIStrings = {
/**
*@description A right-click context menu entry which when clicked causes the menu entry for that player to be removed.
*/
hidePlayer: 'Hide player',
/**
*@description A right-click context menu entry which should keep the element selected, while hiding all other entries.
*/
hideAllOthers: 'Hide all others',
/**
*@description Context menu entry which downloads the json dump when clicked
*/
savePlayerInfo: 'Save player info',
/**
*@description Side-panel entry title text for the players section.
*/
players: 'Players',
};
const str_ = i18n.i18n.registerUIStrings('panels/media/PlayerListView.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export interface PlayerStatus {
playerTitle: string;
playerID: string;
exists: boolean;
playing: boolean;
titleEdited: boolean;
}
export interface PlayerStatusMapElement {
playerStatus: PlayerStatus;
playerTitleElement: HTMLElement|null;
}
export class PlayerEntryTreeElement extends UI.TreeOutline.TreeElement {
titleFromUrl: boolean;
private readonly playerStatus: PlayerStatus;
private readonly displayContainer: MainView;
constructor(playerStatus: PlayerStatus, displayContainer: MainView, playerID: string) {
super(playerStatus.playerTitle, false);
this.titleFromUrl = true;
this.playerStatus = playerStatus;
this.displayContainer = displayContainer;
this.setLeadingIcons([UI.Icon.Icon.create('largeicon-play-animation', 'media-player')]);
this.listItemElement.classList.add('player-entry-tree-element');
this.listItemElement.addEventListener('contextmenu', this.rightClickContextMenu.bind(this, playerID), false);
}
onselect(_selectedByUser?: boolean): boolean {
this.displayContainer.renderMainPanel(this.playerStatus.playerID);
return true;
}
private rightClickContextMenu(playerID: string, event: Event): boolean {
const contextMenu = new UI.ContextMenu.ContextMenu(event);
contextMenu.headerSection().appendItem(i18nString(UIStrings.hidePlayer), this.hidePlayer.bind(this, playerID));
contextMenu.headerSection().appendItem(i18nString(UIStrings.hideAllOthers), this.hideOthers.bind(this, playerID));
contextMenu.headerSection().appendItem(i18nString(UIStrings.savePlayerInfo), this.savePlayer.bind(this, playerID));
contextMenu.show();
return true;
}
private hidePlayer(playerID: string): void {
this.displayContainer.markPlayerForDeletion(playerID);
}
private savePlayer(playerID: string): void {
this.displayContainer.exportPlayerData(playerID);
}
private hideOthers(playerID: string): void {
this.displayContainer.markOtherPlayersForDeletion(playerID);
}
}
export class PlayerListView extends UI.Widget.VBox implements TriggerDispatcher {
private readonly playerStatuses: Map<string, PlayerEntryTreeElement>;
private readonly mainContainer: MainView;
private readonly sidebarTree: UI.TreeOutline.TreeOutlineInShadow;
private readonly playerList: UI.TreeOutline.TreeElement;
constructor(mainContainer: MainView) {
super(true);
this.playerStatuses = new Map();
// Container where new panels can be added based on clicks.
this.mainContainer = mainContainer;
// The parent tree for storing sections
this.sidebarTree = new UI.TreeOutline.TreeOutlineInShadow();
this.contentElement.appendChild(this.sidebarTree.element);
// Players active in this tab.
this.playerList = this.addListSection(i18nString(UIStrings.players));
this.playerList.listItemElement.classList.add('player-entry-header');
}
deletePlayer(playerID: string): void {
this.playerList.removeChild(this.playerStatuses.get(playerID) as UI.TreeOutline.TreeElement);
this.playerStatuses.delete(playerID);
}
private addListSection(title: string): UI.TreeOutline.TreeElement {
const treeElement = new UI.TreeOutline.TreeElement(title, true);
treeElement.listItemElement.classList.add('storage-group-list-item');
treeElement.setCollapsible(false);
treeElement.selectable = false;
this.sidebarTree.appendChild(treeElement);
return treeElement;
}
addMediaElementItem(playerID: string): void {
const playerStatus = {playerTitle: playerID, playerID: playerID, exists: true, playing: false, titleEdited: false};
const playerElement = new PlayerEntryTreeElement(playerStatus, this.mainContainer, playerID);
this.playerStatuses.set(playerID, playerElement);
this.playerList.appendChild(playerElement);
}
setMediaElementPlayerTitle(playerID: string, newTitle: string, isTitleExtractedFromUrl: boolean): void {
if (this.playerStatuses.has(playerID)) {
const sidebarEntry = this.playerStatuses.get(playerID);
if (sidebarEntry && (!isTitleExtractedFromUrl || sidebarEntry.titleFromUrl)) {
sidebarEntry.title = newTitle;
sidebarEntry.titleFromUrl = isTitleExtractedFromUrl;
}
}
}
setMediaElementPlayerIcon(playerID: string, iconName: string): void {
if (this.playerStatuses.has(playerID)) {
const sidebarEntry = this.playerStatuses.get(playerID);
if (!sidebarEntry) {
throw new Error('sidebarEntry is expected to not be null');
}
sidebarEntry.setLeadingIcons([UI.Icon.Icon.create(iconName, 'media-player')]);
}
}
onProperty(playerID: string, property: Protocol.Media.PlayerProperty): void {
// Sometimes the title will be an empty string, since this is provided
// by the website. We don't want to swap title to an empty string.
if (property.name === PlayerPropertyKeys.FrameTitle && property.value) {
this.setMediaElementPlayerTitle(playerID, property.value as string, false);
}
// Url always has a value.
if (property.name === PlayerPropertyKeys.FrameUrl) {
const urlPathComponent = property.value.substring(property.value.lastIndexOf('/') + 1);
this.setMediaElementPlayerTitle(playerID, urlPathComponent, true);
}
}
onError(_playerID: string, _error: Protocol.Media.PlayerError): void {
// TODO(tmathmeyer) show an error icon next to the player name
}
onMessage(_playerID: string, _message: Protocol.Media.PlayerMessage): void {
// TODO(tmathmeyer) show a message count number next to the player name.
}
onEvent(playerID: string, event: PlayerEvent): void {
const eventType = JSON.parse(event.value).event;
if (eventType === 'kPlay') {
this.setMediaElementPlayerIcon(playerID, 'largeicon-play-animation');
} else if (eventType === 'kPause') {
this.setMediaElementPlayerIcon(playerID, 'largeicon-pause-animation');
} else if (eventType === 'kWebMediaPlayerDestroyed') {
this.setMediaElementPlayerIcon(playerID, 'smallicon-videoplayer-destroyed');
}
}
wasShown(): void {
super.wasShown();
this.sidebarTree.registerCSSFiles([playerListViewStyles]);
}
}