/*! @license * Shaka Player * Copyright 2016 Google LLC * SPDX-License-Identifier: Apache-2.0 */ goog.provide('shakaDemo.Custom'); /** @type {?shakaDemo.Custom} */ let shakaDemoCustom; /** * Shaka Player demo, custom asset page layout. */ shakaDemo.Custom = class { /** * Register the page configuration. */ static init() { const elements = shakaDemoMain.addNavButton('custom'); shakaDemoCustom = new shakaDemo.Custom(elements.container); } /** @param {!Element} container */ constructor(container) { /** @private {!HTMLDialogElement} */ this.dialog_ = /** @type {!HTMLDialogElement} */(document.createElement('dialog')); this.dialog_.classList.add('mdl-dialog'); container.appendChild(this.dialog_); if (!this.dialog_.showModal) { dialogPolyfill.registerDialog(this.dialog_); } /** @private {!Set.} */ this.assets_ = this.loadAssetInfos_(); /** @private {!Array.} */ this.assetCards_ = []; this.savedList_ = document.createElement('div'); container.appendChild(this.savedList_); // Add the "new" button, which shows the dialog. const addButtonContainer = document.createElement('div'); addButtonContainer.classList.add('add-button-container'); container.appendChild(addButtonContainer); // Style it as an MDL Floating Action Button (FAB). const addButton = this.makeButton_('add', /* isFAB= */ true, () => { this.showAssetDialog_(ShakaDemoAssetInfo.makeBlankAsset()); }); addButtonContainer.appendChild(addButton); document.addEventListener('shaka-main-selected-asset-changed', () => { this.updateSelected_(); }); document.addEventListener('shaka-main-offline-progress', () => { this.updateOfflineProgress_(); }); document.addEventListener('shaka-main-locale-changed', () => { this.remakeSavedList_(); }); document.addEventListener('shaka-main-page-changed', () => { if (!this.savedList_.childNodes.length && !container.classList.contains('hidden')) { // Now that the page is showing, create the contents that we deferred // until now. this.remakeSavedList_(); } }); } /** @return {!Array.} */ assets() { return Array.from(this.assets_); } /** * Updates progress bars on asset cards. * @private */ updateOfflineProgress_() { for (const card of this.assetCards_) { card.updateProgress(); } } /** * @param {!ShakaDemoAssetInfo} assetInProgress * @private */ showAssetDialog_(assetInProgress) { // Remove buttons for any previous assets. shaka.util.Dom.removeAllChildren(this.dialog_); const inputDiv = document.createElement('div'); this.dialog_.appendChild(inputDiv); const iconDiv = document.createElement('div'); this.dialog_.appendChild(iconDiv); // An array of inputs which have validity checks which we care about. const inputsToCheck = []; // The license server and drm system fields need to know each others // contents, and react to each others changes, to work. // To simplify things, this method picks out the process of setting license // server URLs; it can be called within both fields. let licenseServerUrlInput; let customDrmSystemInput; const setLicenseServerURLs = () => { const licenseServerURL = licenseServerUrlInput.value; const customDRMSystem = customDrmSystemInput.value; if (licenseServerURL) { // Make a license server entry for every common DRM plugin. assetInProgress.licenseServers.clear(); for (const drmSystem of shakaDemo.Main.commonDrmSystems) { assetInProgress.licenseServers.set(drmSystem, licenseServerURL); } if (customDRMSystem) { // Make a custom entry too. assetInProgress.licenseServers.set(customDRMSystem, licenseServerURL); } } else { assetInProgress.licenseServers.clear(); } }; const containerStyle = shakaDemo.InputContainer.Style.VERTICAL; const container = new shakaDemo.InputContainer( inputDiv, /* headerText= */ null, containerStyle, /* docLink= */ null); /** * A utility to simplify the creation of fields on the dialog. * @param {string} name * @param {function(!Element, !Element)} setup * @param {function(!Element)} onChange */ const makeField = (name, setup, onChange) => { container.addRow(null, null); const input = new shakaDemo.TextInput(container, name, onChange); input.extra().textContent = name; setup(input.input(), input.container()); }; // Make the manifest URL field. const manifestSetup = (input, container) => { input.value = assetInProgress.manifestUri; inputsToCheck.push(input); // Make an error that shows up if you did not provide an URL. const error = document.createElement('span'); error.classList.add('mdl-textfield__error'); error.textContent = shakaDemoMain.getLocalizedString( shakaDemo.MessageIds.MANIFEST_URL_ERROR); container.appendChild(error); // Add a regex that will detect empty strings. input.required = true; input.pattern = '^(?!([\r\n\t\f\v ]+)$).*$'; }; const manifestOnChange = (input) => { assetInProgress.manifestUri = input.value; }; const manifestURLName = shakaDemoMain.getLocalizedString( shakaDemo.MessageIds.MANIFEST_URL); makeField(manifestURLName, manifestSetup, manifestOnChange); // Make the license server URL field. const licenseSetup = (input, container) => { licenseServerUrlInput = input; const drmSystems = assetInProgress.licenseServers.keys(); // Custom assets have only a single license server URL, no matter how // many key systems they have. Thus, it's safe to say that the license // server URL associated with the first key system is the asset's // over-all license server URL. const drmSystem = drmSystems.next(); if (drmSystem && drmSystem.value) { input.value = assetInProgress.licenseServers.get(drmSystem.value); } }; const licenseOnChange = (input) => { setLicenseServerURLs(); }; const licenseServerURLName = shakaDemoMain.getLocalizedString( shakaDemo.MessageIds.LICENSE_SERVER_URL); makeField(licenseServerURLName, licenseSetup, licenseOnChange); // Make the license certificate URL field. const certSetup = (input, container) => { if (assetInProgress.certificateUri) { input.value = assetInProgress.certificateUri; } }; const certOnChange = (input) => { assetInProgress.certificateUri = input.value; }; const licenseCertificateURLName = shakaDemoMain.getLocalizedString( shakaDemo.MessageIds.LICENSE_CERTIFICATE_URL); makeField(licenseCertificateURLName, certSetup, certOnChange); // Make the drm system field. const drmSetup = (input, container) => { customDrmSystemInput = input; const drmSystems = assetInProgress.licenseServers.keys(); for (const drmSystem of drmSystems) { if (!shakaDemo.Main.commonDrmSystems.includes(drmSystem)) { input.value = drmSystem; break; } } }; const drmOnChange = (input) => { setLicenseServerURLs(); }; const DRMSystemName = shakaDemoMain.getLocalizedString( shakaDemo.MessageIds.DRM_SYSTEM); makeField(DRMSystemName, drmSetup, drmOnChange); // Make the ad tag URL field. const adTagSetup = (input, container) => { if (assetInProgress.adTagUri) { input.value = assetInProgress.adTagUri; } }; const adTagOnChange = (input) => { assetInProgress.adTagUri = input.value; }; const adTagURLName = shakaDemoMain.getLocalizedString( shakaDemo.MessageIds.AD_TAG_URL); makeField(adTagURLName, adTagSetup, adTagOnChange); // Make the name field. const nameSetup = (input, container) => { input.value = assetInProgress.name; inputsToCheck.push(input); // Make an error that shows up if you have an empty/duplicate name. const error = document.createElement('span'); error.classList.add('mdl-textfield__error'); error.textContent = shakaDemoMain.getLocalizedString( shakaDemo.MessageIds.NAME_ERROR); container.appendChild(error); // Make a regex that will detect duplicates. input.required = true; input.pattern = '^(?!( *'; for (const asset of this.assets_) { if (asset == assetInProgress) { // If editing an existing asset, it's okay if the name doesn't change. continue; } const escape = (input) => { return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }; input.pattern += '|' + escape(asset.name); } input.pattern += ')$).*$'; }; const nameOnChange = (input) => { assetInProgress.name = input.value; }; const nameName = shakaDemoMain.getLocalizedString( shakaDemo.MessageIds.NAME); makeField(nameName, nameSetup, nameOnChange); // Make the icon field. const iconSetup = (input, container) => { if (assetInProgress.iconUri) { input.value = assetInProgress.iconUri; const img = /** @type {!HTMLImageElement} */(document.createElement('img')); img.src = input.value; img.alt = ''; // Not necessary to understand the page iconDiv.appendChild(img); } }; const iconOnChange = (input) => { shaka.util.Dom.removeAllChildren(iconDiv); assetInProgress.iconUri = input.value; if (input.value) { const img = /** @type {!HTMLImageElement} */(document.createElement('img')); img.src = input.value; img.alt = ''; // Not necessary to understand the page iconDiv.appendChild(img); } }; const iconURLName = shakaDemoMain.getLocalizedString( shakaDemo.MessageIds.ICON_URL); makeField(iconURLName, iconSetup, iconOnChange); // Create the buttons at the bottom of the dialog. const buttonsDiv = document.createElement('tr'); inputDiv.appendChild(buttonsDiv); buttonsDiv.appendChild(this.makeButton_('Save', /* isFAB= */ false, () => { for (const input of inputsToCheck) { if (!input.validity.valid) { return; } } shakaDemoMain.setupOfflineSupport(assetInProgress); this.assets_.add(assetInProgress); this.saveAssetInfos_(this.assets_); this.remakeSavedList_(); this.dialog_.close(); })); buttonsDiv.appendChild(this.makeButton_( 'Cancel', /* isFAB= */ false, () => { this.dialog_.close(); })); // Update the componentHandler, to account for the new MDL elements. componentHandler.upgradeDom(); // Show the dialog last, so that it knows where to place it. this.dialog_.showModal(); } /** * @return {!Set.} * @private */ loadAssetInfos_() { const savedString = window.localStorage.getItem(shakaDemo.Custom.saveId_); if (savedString) { const assets = /** @type {!Array.} */(JSON.parse(savedString)); return new Set(assets.map((json) => { const asset = ShakaDemoAssetInfo.fromJSON(json); shakaDemoMain.setupOfflineSupport(asset); return asset; })); } return new Set(); } /** * @param {!Set.} assetInfos * @private */ saveAssetInfos_(assetInfos) { const saveId = shakaDemo.Custom.saveId_; const assets = Array.from(assetInfos); window.localStorage.setItem(saveId, JSON.stringify(assets)); } /** * @param {string} name * @param {boolean} isFAB Should this button be styled as a Material Design * Floating Action Button (FAB)? * @param {function()} callback * @return {!Element} * @private */ makeButton_(name, isFAB, callback) { const button = document.createElement('button'); if (isFAB) { button.classList.add('mdl-button--fab'); button.classList.add('mdl-button--colored'); const icon = document.createElement('i'); icon.classList.add('material-icons-round'); icon.textContent = name; button.appendChild(icon); } else { button.textContent = name; button.classList.add('mdl-button--raised'); } button.addEventListener('click', callback); button.classList.add('mdl-button'); button.classList.add('mdl-js-button'); button.classList.add('mdl-js-ripple-effect'); return button; } /** * @param {!ShakaDemoAssetInfo} asset * @return {!shakaDemo.AssetCard} * @private */ createAssetCardFor_(asset) { const savedList = this.savedList_; const isFeatured = false; return new shakaDemo.AssetCard(savedList, asset, isFeatured, (c) => { c.addButton(shakaDemo.MessageIds.PLAY, () => { shakaDemoMain.loadAsset(asset); this.updateSelected_(); }); c.addButton(shakaDemo.MessageIds.EDIT_CUSTOM, async () => { if (asset.unstoreCallback) { await asset.unstoreCallback(); } this.showAssetDialog_(asset); }); c.addButton(shakaDemo.MessageIds.DELETE_CUSTOM, async () => { this.assets_.delete(asset); if (asset.unstoreCallback) { await asset.unstoreCallback(); } this.saveAssetInfos_(this.assets_); this.remakeSavedList_(); }, shakaDemo.MessageIds.DELETE_CUSTOM); c.addStoreButton(); }); } /** * Updates which asset card is selected. * @private */ updateSelected_() { for (const card of this.assetCards_) { card.selectByAsset(shakaDemoMain.selectedAsset); } } /** @private */ remakeSavedList_() { shaka.util.Dom.removeAllChildren(this.savedList_); if (this.assets_.size == 0) { // Add in a message telling you what to do. const makeMessage = (textClass, text) => { const textElement = document.createElement('h2'); textElement.classList.add('mdl-typography--' + textClass); textElement.textContent = text; this.savedList_.appendChild(textElement); }; makeMessage('title', shakaDemoMain.getLocalizedString( shakaDemo.MessageIds.CUSTOM_INTRO_ONE)); makeMessage('body-2', shakaDemoMain.getLocalizedString( shakaDemo.MessageIds.CUSTOM_INTRO_TWO)); makeMessage('body-1', shakaDemoMain.getLocalizedString( shakaDemo.MessageIds.CUSTOM_INTRO_THREE)); } else { // Make asset cards for the assets. this.assetCards_ = Array.from(this.assets_).map((asset) => { return this.createAssetCardFor_(asset); }); this.updateSelected_(); } } }; /** * The name of the field in window.localStorage that is used to store a user's * custom assets. * @const {string} */ shakaDemo.Custom.saveId_ = 'shakaPlayerDemoSavedAssets'; document.addEventListener('shaka-main-loaded', shakaDemo.Custom.init); document.addEventListener('shaka-main-cleanup', () => { shakaDemoCustom = null; });