blob: 17746becb3284c45c2b31dcabe1b6dc46176d982 [file] [log] [blame]
// 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 'chrome://resources/mojo/mojo/public/js/mojo_bindings_lite.js';
import 'chrome://resources/mojo/mojo/public/mojom/base/big_buffer.mojom-lite.js';
import 'chrome://resources/mojo/mojo/public/mojom/base/string16.mojom-lite.js';
import 'chrome://resources/mojo/mojo/public/mojom/base/unguessable_token.mojom-lite.js';
import 'chrome://resources/mojo/mojo/public/mojom/base/time.mojom-lite.js';
import 'chrome://resources/mojo/url/mojom/url.mojom-lite.js';
import 'chrome://resources/mojo/url/mojom/origin.mojom-lite.js';
import './ui/gfx/geometry/mojom/geometry.mojom-lite.js';
import './media_session.mojom-lite.js';
import './media_history_store.mojom-lite.js';
import {assertNotReached} from 'chrome://resources/js/assert.m.js';
import {decorate} from 'chrome://resources/js/cr/ui.m.js';
import {TabBox} from 'chrome://resources/js/cr/ui/tabs.m.js';
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.m.js';
import {$} from 'chrome://resources/js/util.m.js';
import {MediaDataTable, MediaDataTableDelegate} from './media_data_table.js';
// Allow a function to be provided by tests, which will be called when
// the page has been populated.
const mediaHistoryPageIsPopulatedResolver = new PromiseResolver();
window.whenPageIsPopulatedForTest = function() {
return mediaHistoryPageIsPopulatedResolver.promise;
};
let store = null;
let statsTableBody = null;
let originsTable = null;
let playbacksTable = null;
let sessionsTable = null;
let delegate = null;
/**
* Creates a single row in the stats table.
* @param {string} name The name of the table.
* @param {number} count The row count of the table.
* @return {!Node}
*/
function createStatsRow(name, count) {
const template = $('stats-row');
const td = template.content.querySelectorAll('td');
td[0].textContent = name;
td[1].textContent = count;
return document.importNode(template.content, true);
}
/** @implements {MediaDataTableDelegate} */
class MediaHistoryTableDelegate {
/**
* Formats a field to be displayed in the data table and inserts it into the
* element.
* @param {Element} td
* @param {?Object} data
* @param {string} key
*/
insertDataField(td, data, key) {
if (data === undefined || data === null) {
return;
}
if (key === 'origin') {
// Format a mojo origin.
const {scheme, host, port} = data;
td.textContent = new URL(`${scheme}://${host}:${port}`).origin;
} else if (key === 'lastUpdatedTime') {
// Format a JS timestamp.
td.textContent = data ? new Date(data).toISOString() : '';
} else if (
key === 'cachedAudioVideoWatchtime' ||
key === 'actualAudioVideoWatchtime' || key === 'watchtime' ||
key === 'duration' || key === 'position') {
// Format a mojo timedelta.
const secs = (data.microseconds / 1000000);
td.textContent =
secs.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1,');
} else if (key === 'url') {
// Format a mojo GURL.
td.textContent = data.url;
} else if (key === 'hasAudio' || key === 'hasVideo') {
// Format a boolean.
td.textContent = data ? 'Yes' : 'No';
} else if (
key === 'title' || key === 'artist' || key === 'album' ||
key === 'sourceTitle') {
// Format a mojo string16.
td.textContent = decodeString16(
/** @type {mojoBase.mojom.String16} */ (data));
} else if (key === 'artwork') {
// Format an array of mojo media images.
data.forEach((image) => {
const a = document.createElement('a');
a.href = image.src.url;
a.textContent = image.src.url;
a.target = '_blank';
td.appendChild(a);
td.appendChild(document.createElement('br'));
});
} else {
td.textContent = data;
}
}
/**
* Compares two objects based on |sortKey|.
* @param {string} sortKey The name of the property to sort by.
* @param {?Object} a The first object to compare.
* @param {?Object} b The second object to compare.
* @return {number} A negative number if |a| should be ordered
* before |b|, a positive number otherwise.
*/
compareTableItem(sortKey, a, b) {
const val1 = a[sortKey];
const val2 = b[sortKey];
// Compare the hosts of the origin ignoring schemes.
if (sortKey === 'origin') {
return val1.host > val2.host ? 1 : -1;
}
// Compare the url property.
if (sortKey === 'url') {
return val1.url > val2.url ? 1 : -1;
}
// Compare mojo_base.mojom.TimeDelta microseconds value.
if (sortKey === 'cachedAudioVideoWatchtime' ||
sortKey === 'actualAudioVideoWatchtime' || sortKey === 'watchtime' ||
sortKey === 'duration' || sortKey === 'position') {
return val1.microseconds - val2.microseconds;
}
if (sortKey.startsWith('metadata.')) {
// Keys with a period denote nested objects.
let nestedA = a;
let nestedB = b;
const expandedKey = sortKey.split('.');
expandedKey.forEach((k) => {
nestedA = nestedA[k];
nestedB = nestedB[k];
});
return nestedA > nestedB ? 1 : -1;
}
if (sortKey === 'lastUpdatedTime') {
return val1 - val2;
}
assertNotReached('Unsupported sort key: ' + sortKey);
return 0;
}
}
/**
* Parses utf16 coded string.
* @param {mojoBase.mojom.String16} arr
* @return {string}
*/
function decodeString16(arr) {
if (!arr) {
return '';
}
return arr.data.map(ch => String.fromCodePoint(ch)).join('');
}
/**
* Regenerates the stats table.
* @param {!mediaHistory.mojom.MediaHistoryStats} stats The stats for the Media
* History store.
*/
function renderStatsTable(stats) {
statsTableBody.innerHTML = trustedTypes.emptyHTML;
Object.keys(stats.tableRowCounts).forEach((key) => {
statsTableBody.appendChild(createStatsRow(key, stats.tableRowCounts[key]));
});
}
/**
* @param {!string} name The name of the tab to show.
* @return {Promise}
*/
function showTab(name) {
switch (name) {
case 'stats':
return store.getMediaHistoryStats().then(response => {
renderStatsTable(response.stats);
});
case 'origins':
return store.getMediaHistoryOriginRows().then(response => {
originsTable.setData(response.rows);
});
case 'playbacks':
return store.getMediaHistoryPlaybackRows().then(response => {
playbacksTable.setData(response.rows);
});
case 'sessions':
return store.getMediaHistoryPlaybackSessionRows().then(response => {
sessionsTable.setData(response.rows);
});
}
// Return an empty promise if there is no tab.
return new Promise(() => {});
}
document.addEventListener('DOMContentLoaded', function() {
store = mediaHistory.mojom.MediaHistoryStore.getRemote();
statsTableBody = $('stats-table-body');
delegate = new MediaHistoryTableDelegate();
originsTable = new MediaDataTable($('origins-table'), delegate);
playbacksTable = new MediaDataTable($('playbacks-table'), delegate);
sessionsTable = new MediaDataTable($('sessions-table'), delegate);
decorate('tabbox', TabBox);
// Allow tabs to be navigated to by fragment. The fragment with be of the
// format "#tab-<tab id>".
window.onhashchange = function() {
showTab(window.location.hash.substr(5));
};
// Default to the stats tab.
if (!window.location.hash.substr(5)) {
window.location.hash = 'tab-stats';
} else {
showTab(window.location.hash.substr(5))
.then(mediaHistoryPageIsPopulatedResolver.resolve);
}
// When the tab updates, update the anchor.
$('tabbox').addEventListener('selectedChange', function() {
const tabbox = $('tabbox');
const tabs = tabbox.querySelector('tabs').children;
const selectedTab = tabs[tabbox.selectedIndex];
window.location.hash = 'tab-' + selectedTab.id;
}, true);
// Add handler to 'copy all to clipboard' button.
const copyAllToClipboardButtons =
document.querySelectorAll('.copy-all-to-clipboard');
copyAllToClipboardButtons.forEach((button) => {
button.addEventListener('click', (e) => {
// Make sure nothing is selected.
window.getSelection().removeAllRanges();
document.execCommand('selectAll');
document.execCommand('copy');
// And deselect everything at the end.
window.getSelection().removeAllRanges();
});
});
});