diff --git a/build/types/ui b/build/types/ui index 819e2ac72..8b97439db 100644 --- a/build/types/ui +++ b/build/types/ui @@ -10,6 +10,7 @@ +../../ui/big_play_button.js +../../ui/airplay_button.js +../../ui/cast_button.js ++../../ui/chapter_selection.js +../../ui/context_menu.js +../../ui/controls.js +../../ui/constants.js diff --git a/docs/tutorials/ui-customization.md b/docs/tutorials/ui-customization.md index 2951156eb..8fb10620f 100644 --- a/docs/tutorials/ui-customization.md +++ b/docs/tutorials/ui-customization.md @@ -78,6 +78,7 @@ The following elements can be added to the UI bar using this configuration value only if playing a VR content. * toggle_stereoscopic: adds a button that toggle between monoscopic and stereoscopic. The button is visible only if playing a VR content. +* chapter: adds a button that controls the chapter selection. [Document Picture-in-Picture API]: https://developer.chrome.com/docs/web-platform/document-picture-in-picture/ @@ -106,6 +107,7 @@ The following buttons can be added to the overflow menu: is visible only if playing a VR content. * ad_statistics: adds a button that displays ad statistics of the video. * save_video_frame: adds a button to save the current video frame. +* chapter: adds a button that controls the chapter selection. Example: diff --git a/shaka-player.uncompiled.js b/shaka-player.uncompiled.js index fdb74fe90..beb1a788b 100644 --- a/shaka-player.uncompiled.js +++ b/shaka-player.uncompiled.js @@ -90,6 +90,7 @@ goog.require('shaka.ui.AdStatisticsButton'); goog.require('shaka.ui.AirPlayButton'); goog.require('shaka.ui.BigPlayButton'); goog.require('shaka.ui.CastButton'); +goog.require('shaka.ui.ChapterSelection'); goog.require('shaka.ui.ContextMenu'); goog.require('shaka.ui.Element'); goog.require('shaka.ui.FastForwardButton'); diff --git a/ui/chapter_selection.js b/ui/chapter_selection.js new file mode 100644 index 000000000..223a07470 --- /dev/null +++ b/ui/chapter_selection.js @@ -0,0 +1,179 @@ +/*! @license + * Shaka Player + * Copyright 2016 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + + +goog.provide('shaka.ui.ChapterSelection'); + +goog.require('shaka.ui.Controls'); +goog.require('shaka.ui.Enums'); +goog.require('shaka.ui.Locales'); +goog.require('shaka.ui.Localization'); +goog.require('shaka.ui.OverflowMenu'); +goog.require('shaka.ui.SettingsMenu'); +goog.require('shaka.ui.Utils'); +goog.require('shaka.util.Dom'); +goog.requireType('shaka.ui.Controls'); + +/** + * @extends {shaka.ui.SettingsMenu} + * @final + * @export + */ +shaka.ui.ChapterSelection = class extends shaka.ui.SettingsMenu { + /** + * @param {!HTMLElement} parent + * @param {!shaka.ui.Controls} controls + */ + constructor(parent, controls) { + super(parent, controls, shaka.ui.Enums.MaterialDesignIcons.CHAPTER); + + this.button.classList.add('shaka-chapter-button'); + this.menu.classList.add('shaka-chapters'); + this.button.classList.add('shaka-tooltip-status'); + + /** @type {!Array} */ + this.chapters_ = []; + + this.chaptersLanguage_ = 'und'; + + this.eventManager.listen( + this.localization, shaka.ui.Localization.LOCALE_UPDATED, () => { + this.updateLocalizedStrings_(); + this.updateChapters_(); + }); + + this.eventManager.listen( + this.localization, shaka.ui.Localization.LOCALE_CHANGED, () => { + this.updateLocalizedStrings_(); + this.updateChapters_(); + }); + + this.eventManager.listen(this.player, 'unloading', () => { + this.deletePreviousChapters_(); + this.chaptersLanguage_ = 'und'; + this.chapters_ = []; + }); + + this.eventManager.listen(this.player, 'trackschanged', () => { + this.updateChapters_(); + }); + + // Set up all the strings in the user's preferred language. + this.updateLocalizedStrings_(); + + this.updateChapters_(); + } + + /** + * @private + */ + updateLocalizedStrings_() { + const LocIds = shaka.ui.Locales.Ids; + + this.backButton.ariaLabel = this.localization.resolve(LocIds.BACK); + this.button.ariaLabel = this.localization.resolve(LocIds.CHAPTERS); + this.nameSpan.textContent = this.localization.resolve(LocIds.CHAPTERS); + this.backSpan.textContent = this.localization.resolve(LocIds.CHAPTERS); + } + + /** + * @private + */ + deletePreviousChapters_() { + // 1. Save the back to menu button + const backButton = shaka.ui.Utils.getFirstDescendantWithClassName( + this.menu, 'shaka-back-to-overflow-button'); + + // 2. Remove everything + shaka.util.Dom.removeAllChildren(this.menu); + + // 3. Add the backTo Menu button back + this.menu.appendChild(backButton); + + // 4. Hidden button + shaka.ui.Utils.setDisplay(this.button, false); + } + + /** + * @private + */ + updateChapters_() { + /** + * Does a value compare on chapters. + * @param {shaka.extern.Chapter} a + * @param {shaka.extern.Chapter} b + * @return {boolean} + */ + const chaptersEqual = (a, b) => { + return (!a && !b) || (a.id === b.id && a.title === b.title && + a.startTime === b.startTime && a.endTime === b.endTime); + }; + + let nextLanguage = 'und'; + /** @type {!Array} */ + let nextChapters = []; + + const currentLocales = this.localization.getCurrentLocales(); + for (const locale of Array.from(currentLocales)) { + nextLanguage = locale; + nextChapters = this.player.getChapters(nextLanguage); + if (nextChapters.length) { + break; + } + } + if (!nextChapters.length) { + nextLanguage = 'und'; + nextChapters = this.player.getChapters(nextLanguage); + } + + const languageChanged = nextLanguage !== this.chaptersLanguage_; + const chaptersChanged = this.chapters_.length !== nextChapters.length || + !this.chapters_.some((c, idx) => { + const n = nextChapters.at(idx); + return chaptersEqual(c, n) || + nextChapters.some((n) => chaptersEqual(c, n)); + }); + + this.chaptersLanguage_ = nextLanguage; + this.chapters_ = nextChapters; + if (!nextChapters.length) { + this.deletePreviousChapters_(); + } else if (languageChanged || chaptersChanged) { + for (const chapter of this.chapters_) { + const button = shaka.util.Dom.createButton(); + const span = shaka.util.Dom.createHTMLElement('span'); + span.classList.add('shaka-chapter'); + span.textContent = chapter.title; + button.appendChild(span); + + this.eventManager.listen(button, 'click', () => { + this.video.currentTime = chapter.startTime; + }); + + this.menu.appendChild(button); + } + shaka.ui.Utils.setDisplay(this.button, true); + shaka.ui.Utils.focusOnTheChosenItem(this.menu); + } + } +}; + +/** + * @implements {shaka.extern.IUIElement.Factory} + * @final + */ +shaka.ui.ChapterSelection.Factory = class { + /** @override */ + create(rootElement, controls) { + return new shaka.ui.ChapterSelection(rootElement, controls); + } +}; + +shaka.ui.OverflowMenu.registerElement( + 'chapter', new shaka.ui.ChapterSelection.Factory()); + +shaka.ui.Controls.registerElement( + 'chapter', new shaka.ui.ChapterSelection.Factory()); diff --git a/ui/enums.js b/ui/enums.js index a117cc53f..20ea40850 100644 --- a/ui/enums.js +++ b/ui/enums.js @@ -47,4 +47,5 @@ shaka.ui.Enums.MaterialDesignIcons = { 'RECENTER_VR': 'control_camera', 'TOGGLE_STEREOSCOPIC': '3d_rotation', 'DOWNLOAD': 'download', + 'CHAPTER': 'bookmarks', }; diff --git a/ui/less/overflow_menu.less b/ui/less/overflow_menu.less index 7376a4525..c6a715c36 100644 --- a/ui/less/overflow_menu.less +++ b/ui/less/overflow_menu.less @@ -118,6 +118,11 @@ /* TODO(b/116651454): eliminate hard-coded offsets */ margin-left: 54px; } + + .shaka-chapter { + /* TODO(b/116651454): eliminate hard-coded offsets */ + margin-left: 10px; + } } /* This is a button within each submenu that takes you back to the main overflow diff --git a/ui/locales/en.json b/ui/locales/en.json index 9ed634ebf..fea69cda2 100644 --- a/ui/locales/en.json +++ b/ui/locales/en.json @@ -8,6 +8,7 @@ "BACK": "Back", "CAPTIONS": "Captions", "CAST": "Cast...", + "CHAPTERS": "Chapters", "DOWNLOAD_VIDEO_FRAME": "Save video frame", "ENTER_LOOP_MODE": "Loop the current video", "ENTER_PICTURE_IN_PICTURE": "Enter Picture-in-Picture", diff --git a/ui/locales/es.json b/ui/locales/es.json index d13a05a47..1c81313ee 100644 --- a/ui/locales/es.json +++ b/ui/locales/es.json @@ -8,6 +8,7 @@ "BACK": "Atrás", "CAPTIONS": "Subtítulos", "CAST": "Enviar...", + "CHAPTERS": "Capítulos", "DOWNLOAD_VIDEO_FRAME": "Guardar fotograma", "ENTER_LOOP_MODE": "Reproducir en bucle el vídeo actual", "ENTER_PICTURE_IN_PICTURE": "Activar el modo imagen en imagen", diff --git a/ui/locales/fr.json b/ui/locales/fr.json index 5df772f76..f2ab3319a 100644 --- a/ui/locales/fr.json +++ b/ui/locales/fr.json @@ -8,6 +8,7 @@ "BACK": "Retour", "CAPTIONS": "Sous-titres", "CAST": "Caster…", + "CHAPTERS": "Chapitres", "DOWNLOAD_VIDEO_FRAME": "Enregistrer l'image vidéo", "ENTER_LOOP_MODE": "Lire en boucle la vidéo en cours", "ENTER_PICTURE_IN_PICTURE": "Utiliser le mode Picture-in-Picture", diff --git a/ui/locales/source.json b/ui/locales/source.json index 728121a88..79b7b05b2 100644 --- a/ui/locales/source.json +++ b/ui/locales/source.json @@ -37,6 +37,10 @@ "description": "Label for a button used to open the native Cast dialog in the browser and select a destination to Cast to.", "message": "Cast..." }, + "CHAPTERS": { + "description": "Label for a button used to open a submenu to choose a chapter.", + "message": "Chapters" + }, "DOWNLOAD_VIDEO_FRAME": { "description": "Label for a button used to download the current video frame.", "message": "Save video frame" diff --git a/ui/ui.js b/ui/ui.js index 24b0a5f9c..e7f7b3099 100644 --- a/ui/ui.js +++ b/ui/ui.js @@ -187,6 +187,7 @@ shaka.ui.Overlay = class { 'captions', 'quality', 'language', + 'chapter', 'picture_in_picture', 'cast', 'playback_rate',