From 415ebdc52e1d67729aba0fa89c1421885dbdcf66 Mon Sep 17 00:00:00 2001 From: Aditya Mishra <66531777+aditya0155@users.noreply.github.com> Date: Tue, 12 May 2026 16:54:17 +0530 Subject: [PATCH] feat(UI): Add live subtitle style preview on hover (#10077) --- build/types/ui | 1 + lib/player.js | 10 ++ lib/text/ui_text_displayer.js | 133 +++++++++++++++-- test/cast/cast_utils_unit.js | 1 + test/text/ui_text_displayer_unit.js | 97 +++++++++++- test/ui/ui_unit.js | 224 ++++++++++++++++++++++++++++ ui/context_menu.js | 1 + ui/controls.js | 42 ++++++ ui/locales/ar-XB.json | 1 + ui/locales/ar.json | 1 + ui/locales/be.json | 1 + ui/locales/bg.json | 1 + ui/locales/ca.json | 1 + ui/locales/cs.json | 1 + ui/locales/da.json | 1 + ui/locales/de.json | 1 + ui/locales/el.json | 1 + ui/locales/en-GB.json | 1 + ui/locales/en-XA.json | 1 + ui/locales/en.json | 1 + ui/locales/es-419.json | 1 + ui/locales/es.json | 1 + ui/locales/fa.json | 1 + ui/locales/fi.json | 1 + ui/locales/fil.json | 1 + ui/locales/fr.json | 1 + ui/locales/hi.json | 1 + ui/locales/hr.json | 1 + ui/locales/hu.json | 1 + ui/locales/id.json | 1 + ui/locales/it.json | 1 + ui/locales/iw.json | 1 + ui/locales/ja.json | 1 + ui/locales/ko.json | 1 + ui/locales/lt.json | 1 + ui/locales/lv.json | 1 + ui/locales/nl.json | 1 + ui/locales/no.json | 1 + ui/locales/oc.json | 3 +- ui/locales/pl.json | 1 + ui/locales/pt-BR.json | 1 + ui/locales/pt-PT.json | 1 + ui/locales/ro.json | 1 + ui/locales/ru.json | 1 + ui/locales/sjn.json | 1 + ui/locales/sk.json | 1 + ui/locales/sl.json | 1 + ui/locales/source.json | 4 + ui/locales/sr.json | 1 + ui/locales/sv.json | 1 + ui/locales/th.json | 1 + ui/locales/tr.json | 1 + ui/locales/uk.json | 1 + ui/locales/vi.json | 1 + ui/locales/zh-HK.json | 1 + ui/locales/zh-TW.json | 1 + ui/locales/zh.json | 1 + ui/settings_menu.js | 55 +++++++ ui/text_position.js | 16 ++ ui/text_size.js | 16 ++ ui/text_style_preview.js | 198 ++++++++++++++++++++++++ ui/ui_utils.js | 15 ++ 62 files changed, 847 insertions(+), 16 deletions(-) create mode 100644 ui/text_style_preview.js diff --git a/build/types/ui b/build/types/ui index a5b59e308..518950da7 100644 --- a/build/types/ui +++ b/build/types/ui @@ -46,6 +46,7 @@ +../../ui/text_position.js +../../ui/text_selection.js +../../ui/text_size.js ++../../ui/text_style_preview.js +../../ui/toggle_stereoscopic.js +../../ui/ui.js +../../ui/ui_utils.js diff --git a/lib/player.js b/lib/player.js index caa546c4e..b3f69dcaf 100644 --- a/lib/player.js +++ b/lib/player.js @@ -7640,6 +7640,16 @@ shaka.Player = class extends shaka.util.FakeEventTarget { return this.videoContainer_; } + /** + * Returns the active text displayer, if one has been created. This is not + * exported; it is for internal integrations such as the UI controls. + * + * @return {?shaka.extern.TextDisplayer} + */ + getTextDisplayer() { + return this.textDisplayer_; + } + /** * @param {!shaka.util.Error} error * @private diff --git a/lib/text/ui_text_displayer.js b/lib/text/ui_text_displayer.js index 37458da7d..31ba1f3ec 100644 --- a/lib/text/ui_text_displayer.js +++ b/lib/text/ui_text_displayer.js @@ -56,6 +56,15 @@ shaka.text.UITextDisplayer = class { /** @private {?shaka.extern.TextDisplayerConfiguration} */ this.config_ = null; + /** @private {?shaka.extern.TextDisplayerConfiguration} */ + this.previewConfig_ = null; + + /** @private {?shaka.text.Cue} */ + this.previewCue_ = null; + + /** @private {boolean} */ + this.showingPreviewCue_ = false; + /** @type {HTMLElement} */ this.textContainer_ = shaka.util.Dom.createHTMLElement('div'); this.textContainer_.classList.add('shaka-text-container'); @@ -231,6 +240,38 @@ shaka.text.UITextDisplayer = class { this.updateCaptions_(/* forceUpdate= */ true); } + /** + * Temporarily previews text displayer style settings using the normal UI + * text rendering path. + * + * @param {!shaka.extern.TextDisplayerConfiguration} config + * @param {string} exampleText + * @export + */ + setTextStylePreview(config, exampleText) { + this.previewConfig_ = + /** @type {!shaka.extern.TextDisplayerConfiguration} */( + Object.assign({}, config)); + this.previewCue_ = new shaka.text.Cue( + Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, exampleText); + this.updateCaptions_(/* forceUpdate= */ true); + } + + /** + * Clears temporary text style preview settings. + * @export + */ + clearTextStylePreview() { + if (!this.previewConfig_ && !this.previewCue_) { + return; + } + + this.previewConfig_ = null; + this.previewCue_ = null; + this.showingPreviewCue_ = false; + this.updateCaptions_(/* forceUpdate= */ true); + } + /** * @override @@ -278,6 +319,9 @@ shaka.text.UITextDisplayer = class { this.isTextVisible_ = false; this.cues_ = []; + this.previewConfig_ = null; + this.previewCue_ = null; + this.showingPreviewCue_ = false; this.captionsTimer_?.stop(); this.captionsTimer_ = null; this.applyVisibilityTimer_?.stop(); @@ -381,6 +425,14 @@ shaka.text.UITextDisplayer = class { } } + /** + * @return {?shaka.extern.TextDisplayerConfiguration} + * @private + */ + getActiveConfig_() { + return this.previewConfig_ || this.config_; + } + /** * @private */ @@ -450,6 +502,8 @@ shaka.text.UITextDisplayer = class { * @private */ updateCuesRecursive_(cues, container, currentTime, parents) { + const config = this.getActiveConfig_(); + // Set to true if the cues have changed in some way, which will require // DOM changes. E.g. if a cue was added or removed. let updateDOM = false; @@ -474,8 +528,8 @@ shaka.text.UITextDisplayer = class { let cueKey = cue; let cueRegistry = this.currentCuesMap_.get(cue); - if (!cueRegistry && this.config_ && - this.config_.positionArea != shaka.config.PositionArea.DEFAULT) { + if (!cueRegistry && config && + config.positionArea != shaka.config.PositionArea.DEFAULT) { for (const key of this.currentCuesMap_.keys()) { if (shaka.text.Cue.equal(cue, key)) { cueKey = key; @@ -551,8 +605,8 @@ shaka.text.UITextDisplayer = class { }); for (const cue of toPlant) { let cueRegistry = this.currentCuesMap_.get(cue); - if (!cueRegistry && - this.config_.positionArea != shaka.config.PositionArea.DEFAULT) { + if (!cueRegistry && config && + config.positionArea != shaka.config.PositionArea.DEFAULT) { for (const key of this.currentCuesMap_.keys()) { if (shaka.text.Cue.equal(cue, key)) { cueRegistry = this.currentCuesMap_.get(key); @@ -573,6 +627,30 @@ shaka.text.UITextDisplayer = class { } } + /** + * @param {!Array} cues + * @param {number} currentTime + * @return {boolean} + * @private + */ + hasCueAtTime_(cues, currentTime) { + return cues.some((cue) => { + return cue.startTime <= currentTime && cue.endTime > currentTime && + this.hasCueContent_(cue); + }); + } + + /** + * @param {!shaka.text.Cue} cue + * @return {boolean} + * @private + */ + hasCueContent_(cue) { + const text = cue.payload.replace(/[\u00a0\u200B]/g, ' ').trim(); + return !!text || !!cue.backgroundImage || + cue.nestedCues.some((nestedCue) => this.hasCueContent_(nestedCue)); + } + /** * Display the current captions. * @param {boolean=} forceUpdate @@ -583,9 +661,34 @@ shaka.text.UITextDisplayer = class { return; } - const delay = this.config_?.subtitleDelay ?? 0; + const config = this.getActiveConfig_(); + const delay = config?.subtitleDelay ?? 0; const currentTime = this.video_.currentTime - delay; - if (!this.isTextVisible_ || forceUpdate) { + const showPreviewCue = !!this.previewCue_ && + !this.hasCueAtTime_(this.cues_, currentTime); + if (showPreviewCue != this.showingPreviewCue_) { + forceUpdate = true; + this.showingPreviewCue_ = showPreviewCue; + } + + if (showPreviewCue) { + // Force a full clear if we are showing a preview. This prevents + // "stacking" bugs because the preview cue object changes + // frequently during hover. + forceUpdate = true; + } + const shouldBeVisible = this.isTextVisible_ || showPreviewCue; + if (shouldBeVisible) { + if (!this.textContainer_.parentElement) { + this.videoContainer_.appendChild(this.textContainer_); + } + } else { + if (this.textContainer_.parentElement) { + this.videoContainer_.removeChild(this.textContainer_); + } + } + + if (!shouldBeVisible || forceUpdate) { // Remove child elements from all regions. for (const regionElement of this.regionElements_.values()) { shaka.util.Dom.removeAllChildren(regionElement); @@ -596,7 +699,7 @@ shaka.text.UITextDisplayer = class { this.currentCuesMap_.clear(); this.regionElements_.clear(); } - if (this.isTextVisible_) { + if (shouldBeVisible) { // Log currently attached cue elements for verification, later. const previousCuesMap = new Map(); if (goog.DEBUG) { @@ -605,9 +708,10 @@ shaka.text.UITextDisplayer = class { } } - let cues = this.cues_; - if (this.config_ && - this.config_.positionArea != shaka.config.PositionArea.DEFAULT) { + let cues = showPreviewCue && this.previewCue_ ? + [this.previewCue_] : this.cues_; + if (config && + config.positionArea != shaka.config.PositionArea.DEFAULT) { cues = cues.map((cue) => this.processCueStyle_(cue)); } @@ -634,13 +738,15 @@ shaka.text.UITextDisplayer = class { /** @private */ processCueStyle_(cue) { + const config = /** @type {!shaka.extern.TextDisplayerConfiguration} */( + this.getActiveConfig_()); goog.asserts.assert( - this.config_.positionArea !== shaka.config.PositionArea.DEFAULT, + config.positionArea !== shaka.config.PositionArea.DEFAULT, 'processCueStyle_ is intended to use on non default positioning'); const modifiedCue = cue.clone(); shaka.text.Utils.resetCuePositioning(modifiedCue); modifiedCue.region = shaka.text.UITextDisplayer.CustomRegion_.value(); - switch (this.config_.positionArea) { + switch (config.positionArea) { case shaka.config.PositionArea.TOP_LEFT: modifiedCue.textAlign = shaka.text.Cue.textAlign.LEFT; modifiedCue.displayAlign = shaka.text.Cue.displayAlign.BEFORE; @@ -1011,7 +1117,8 @@ shaka.text.UITextDisplayer = class { style.fontWeight = cue.fontWeight.toString(); style.fontStyle = cue.fontStyle; style.letterSpacing = cue.letterSpacing; - const fontScaleFactor = this.config_ ? this.config_.fontScaleFactor : 1; + const config = this.getActiveConfig_(); + const fontScaleFactor = config ? config.fontScaleFactor : 1; if (fontScaleFactor !== 1 || cue.fontSize) { // Use browser default (1em) if fontSize is not set or empty const fontSize = cue.fontSize || '1em'; diff --git a/test/cast/cast_utils_unit.js b/test/cast/cast_utils_unit.js index 1e12fe832..dcaddf847 100644 --- a/test/cast/cast_utils_unit.js +++ b/test/cast/cast_utils_unit.js @@ -20,6 +20,7 @@ describe('CastUtils', () => { 'getNetworkingEngine', // Handled specially 'getDrmEngine', // Handled specially 'getMediaElement', // Handled specially + 'getTextDisplayer', // Internal UI hook, not proxied. 'destroy', // Should use CastProxy.destroy instead 'drmInfo', // Too large to proxy 'getManifest', // Too large to proxy diff --git a/test/text/ui_text_displayer_unit.js b/test/text/ui_text_displayer_unit.js index bdb545107..7417efd6f 100644 --- a/test/text/ui_text_displayer_unit.js +++ b/test/text/ui_text_displayer_unit.js @@ -631,13 +631,106 @@ describe('UITextDisplayer', () => { expect(videoContainer.childNodes.length).toBe(0); }); + it('previews text styles through the normal renderer', () => { + const cue = new shaka.text.Cue(0, 100, 'Previewed cue'); + const config = + shaka.util.PlayerConfiguration.createDefault().textDisplayer; + + textDisplayer.configure(config); + textDisplayer.setTextVisibility(true); + textDisplayer.append([cue]); + updateCaptions(); + + const textContainer = videoContainer.querySelector('.shaka-text-container'); + let cueElement = textContainer.querySelector('div'); + expect(cueElement.textContent).toBe('Previewed cue'); + expect(cueElement.style.fontSize).toBe(''); + + const previewConfig = + /** @type {!shaka.extern.TextDisplayerConfiguration} */( + Object.assign({}, config, {'fontScaleFactor': 2})); + textDisplayer.setTextStylePreview( + previewConfig, 'Subtitles example'); + + cueElement = textContainer.querySelector('div'); + expect(cueElement.textContent).toBe('Previewed cue'); + expect(cueElement.style.fontSize).toBe('2em'); + + textDisplayer.clearTextStylePreview(); + + cueElement = textContainer.querySelector('div'); + expect(cueElement.textContent).toBe('Previewed cue'); + expect(cueElement.style.fontSize).toBe(''); + }); + + it('shows example text only when no cue is active during preview', () => { + const config = + shaka.util.PlayerConfiguration.createDefault().textDisplayer; + + textDisplayer.configure(config); + textDisplayer.setTextVisibility(true); + + textDisplayer.setTextStylePreview(config, 'Subtitles example'); + + const textContainer = videoContainer.querySelector('.shaka-text-container'); + expect(textContainer.textContent).toBe('Subtitles example'); + + textDisplayer.clearTextStylePreview(); + expect(textContainer.textContent).toBe(''); + + textDisplayer.setTextStylePreview(config, 'Subtitles example'); + expect(textContainer.textContent).toBe('Subtitles example'); + + textDisplayer.append([new shaka.text.Cue(0, 100, 'Real subtitle')]); + updateCaptions(); + + expect(textContainer.textContent).toBe('Real subtitle'); + }); + + it('shows example text while normal text visibility is off', () => { + const config = + shaka.util.PlayerConfiguration.createDefault().textDisplayer; + + textDisplayer.configure(config); + + textDisplayer.setTextStylePreview(config, 'Subtitles example'); + + const textContainer = videoContainer.querySelector('.shaka-text-container'); + expect(textContainer.textContent).toBe('Subtitles example'); + + textDisplayer.clearTextStylePreview(); + + expect(videoContainer.querySelector('.shaka-text-container')).toBe(null); + }); + + it('replaces example text during repeated preview updates', () => { + const config = + shaka.util.PlayerConfiguration.createDefault().textDisplayer; + + textDisplayer.configure(config); + textDisplayer.setTextVisibility(true); + + textDisplayer.setTextStylePreview(config, 'First example'); + + const textContainer = videoContainer.querySelector('.shaka-text-container'); + let cueElements = textContainer.querySelectorAll('div'); + expect(cueElements.length).toBe(1); + expect(textContainer.textContent).toBe('First example'); + + textDisplayer.setTextStylePreview(config, 'Second example'); + + cueElements = textContainer.querySelectorAll('div'); + expect(cueElements.length).toBe(1); + expect(textContainer.textContent).toBe('Second example'); + }); + it('positions cue at top-left when positionArea=TOP_LEFT', () => { /** @type {!shaka.text.Cue} */ const cue = new shaka.text.Cue(0, 100, 'Top-Left'); textDisplayer.setTextVisibility(true); - const player = new shaka.Player(); - const config = player.getConfiguration().textDisplayer; + const config = + shaka.util.PlayerConfiguration.createDefault().textDisplayer; config.positionArea = shaka.config.PositionArea.TOP_LEFT; textDisplayer.configure(config); diff --git a/test/ui/ui_unit.js b/test/ui/ui_unit.js index 96d5cafb2..9e04dcc67 100644 --- a/test/ui/ui_unit.js +++ b/test/ui/ui_unit.js @@ -656,6 +656,230 @@ describe('UI', () => { }); }); + describe('caption style preview', () => { + /** @type {shaka.ui.Controls} */ + let controls; + /** @type {!jasmine.Spy} */ + let setPreviewSpy; + /** @type {!jasmine.Spy} */ + let clearPreviewSpy; + + /** + * @param {!HTMLElement} menu + * @param {string} label + * @return {!HTMLElement} + */ + function getStyleOption(menu, label) { + const buttons = Array.from( + menu.querySelectorAll('button[role="menuitemradio"]')); + const button = buttons.find((button) => { + const span = button.querySelector('span'); + return span && span.textContent == label; + }); + expect(button).not.toBe(undefined); + return /** @type {!HTMLElement} */(button); + } + + /** + * @return {!shaka.extern.TextDisplayerConfiguration} + */ + function latestPreviewConfig() { + const calls = setPreviewSpy.calls; + expect(calls.count()).toBeGreaterThan(0); + return /** @type {!shaka.extern.TextDisplayerConfiguration} */( + calls.mostRecent().args[0]); + } + + /** + * @param {?shaka.Player} player + */ + function usePreviewTextDisplayer(player) { + expect(player).not.toBe(null); + const localPlayer = /** @type {!shaka.Player} */(player); + /** @type {?} */ + const textDisplayer = {}; + setPreviewSpy = textDisplayer.setTextStylePreview = + jasmine.createSpy('setTextStylePreview'); + clearPreviewSpy = textDisplayer.clearTextStylePreview = + jasmine.createSpy('clearTextStylePreview'); + spyOn(localPlayer, 'getTextDisplayer').and.returnValue( + /** @type {!shaka.extern.TextDisplayer} */(textDisplayer)); + } + + it('does not require player or displayer preview methods', () => { + const player = /** @type {?} */(new shaka.util.FakeEventTarget()); + player.getConfiguration = () => { + return shaka.util.PlayerConfiguration.createDefault(); + }; + const localization = new shaka.ui.Localization('en'); + shaka.ui.Locales.addTo(localization); + const preview = new shaka.ui.TextStylePreview( + /** @type {!shaka.Player} */(player), localization); + + expect(() => { + preview.show(); + preview.update({'fontScaleFactor': 2}); + preview.reset(); + preview.hide(); + }).not.toThrow(); + + preview.release(); + }); + + it('updates and reverts font size on hover', async () => { + const config = { + controlPanelElements: [ + 'captions-size', + ], + customContextMenu: false, + }; + const ui = await UiUtils.createUIThroughAPI( + videoContainer, video, config); + controls = ui.getControls(); + player = controls.getLocalPlayer(); + usePreviewTextDisplayer(player); + player.configure('textDisplayer.fontScaleFactor', 1.25); + controls.showUI(); + + const menu = UiUtils.getElementByClassName( + videoContainer, 'shaka-text-positions'); + const button = UiUtils.getElementByClassName( + videoContainer, 'shaka-caption-size-button'); + button.click(); + + expect(latestPreviewConfig().fontScaleFactor).toBe(1.25); + + const largerOption = getStyleOption(menu, '200%'); + UiUtils.simulateEvent(largerOption, 'mouseenter'); + expect(latestPreviewConfig().fontScaleFactor).toBe(2); + + UiUtils.simulateEvent(largerOption, 'mouseleave'); + expect(latestPreviewConfig().fontScaleFactor).toBe(1.25); + + const selectedOption = getStyleOption(menu, '150%'); + selectedOption.click(); + expect(latestPreviewConfig().fontScaleFactor).toBe(1.5); + + UiUtils.simulateEvent(selectedOption, 'mouseleave'); + expect(latestPreviewConfig().fontScaleFactor).toBe(1.5); + + controls.hideSettingsMenus(); + }); + + it('updates and reverts text position on focus', async () => { + const config = { + controlPanelElements: [ + 'captions-position', + ], + customContextMenu: false, + }; + const ui = await UiUtils.createUIThroughAPI( + videoContainer, video, config); + controls = ui.getControls(); + player = controls.getLocalPlayer(); + usePreviewTextDisplayer(player); + controls.showUI(); + + const menu = UiUtils.getElementByClassName( + videoContainer, 'shaka-text-positions'); + const button = UiUtils.getElementByClassName( + videoContainer, 'shaka-caption-position-button'); + button.click(); + + expect(latestPreviewConfig().positionArea) + .toBe(shaka.config.PositionArea.DEFAULT); + + const topLeftOption = getStyleOption(menu, 'Top left'); + topLeftOption.dispatchEvent(new Event('focus')); + expect(latestPreviewConfig().positionArea) + .toBe(shaka.config.PositionArea.TOP_LEFT); + + topLeftOption.dispatchEvent(new Event('blur')); + expect(latestPreviewConfig().positionArea) + .toBe(shaka.config.PositionArea.DEFAULT); + }); + + it('keeps a committed text size as the preview baseline', async () => { + const config = { + controlPanelElements: [ + 'captions-size', + ], + customContextMenu: false, + }; + const ui = await UiUtils.createUIThroughAPI( + videoContainer, video, config); + controls = ui.getControls(); + player = controls.getLocalPlayer(); + usePreviewTextDisplayer(player); + player.configure('textDisplayer.fontScaleFactor', 1.25); + controls.showUI(); + + const menu = UiUtils.getElementByClassName( + videoContainer, 'shaka-text-positions'); + const button = UiUtils.getElementByClassName( + videoContainer, 'shaka-caption-size-button'); + button.click(); + + const largerOption = getStyleOption(menu, '200%'); + UiUtils.simulateEvent(largerOption, 'mouseenter'); + expect(latestPreviewConfig().fontScaleFactor).toBe(2); + + player.configure('textDisplayer.fontScaleFactor', 1.5); + expect(latestPreviewConfig().fontScaleFactor).toBe(2); + + UiUtils.simulateEvent(largerOption, 'mouseleave'); + expect(latestPreviewConfig().fontScaleFactor).toBe(1.5); + }); + + it('hides the preview when a context menu closes', async () => { + const config = { + controlPanelElements: [], + contextMenuElements: [ + 'captions-size', + ], + customContextMenu: true, + }; + const ui = await UiUtils.createUIThroughAPI( + videoContainer, video, config); + controls = ui.getControls(); + player = controls.getLocalPlayer(); + usePreviewTextDisplayer(player); + controls.showUI(); + + const controlsContainer = controls.getControlsContainer(); + UiUtils.simulateEvent(controlsContainer, 'contextmenu'); + + const captionsSizeButton = UiUtils.getElementByClassName( + videoContainer, 'shaka-caption-size-button'); + captionsSizeButton.click(); + + UiUtils.simulateEvent(controlsContainer, 'click'); + expect(clearPreviewSpy).toHaveBeenCalledTimes(1); + }); + + it('hides the preview when the UI is reconfigured', async () => { + const config = { + controlPanelElements: [ + 'captions-size', + ], + customContextMenu: false, + }; + const ui = await UiUtils.createUIThroughAPI( + videoContainer, video, config); + controls = ui.getControls(); + player = controls.getLocalPlayer(); + usePreviewTextDisplayer(player); + controls.showUI(); + + const button = UiUtils.getElementByClassName( + videoContainer, 'shaka-caption-size-button'); + button.click(); + + ui.configure('showUIAlways', true); + expect(clearPreviewSpy).toHaveBeenCalledTimes(1); + }); + }); + describe('resolutions menu', () => { /** @type {!HTMLElement} */ let resolutionsMenu; diff --git a/ui/context_menu.js b/ui/context_menu.js index 095815ac7..c4b7a5333 100644 --- a/ui/context_menu.js +++ b/ui/context_menu.js @@ -132,6 +132,7 @@ shaka.ui.ContextMenu = class extends shaka.ui.Element { */ closeMenu() { shaka.ui.Utils.setDisplay(this.contextMenu_, false); + this.controls.hideTextStylePreview(); } /** diff --git a/ui/controls.js b/ui/controls.js index dd264cfd0..8025ba951 100644 --- a/ui/controls.js +++ b/ui/controls.js @@ -26,6 +26,7 @@ goog.require('shaka.ui.Localization'); goog.require('shaka.ui.MediaSession'); goog.require('shaka.ui.SeekBar'); goog.require('shaka.ui.SkipAdButton'); +goog.require('shaka.ui.TextStylePreview'); goog.require('shaka.ui.Utils'); goog.require('shaka.ui.VRManager'); goog.require('shaka.util.ArrayUtils'); @@ -290,6 +291,8 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget { for (const menu of this.menus_) { shaka.ui.Utils.setDisplay(menu, /* visible= */ false); } + this.dispatchEvent(new shaka.util.FakeEvent('submenuclose')); + this.hideTextStylePreview(); if (this.config_.enableTooltips) { this.controlsButtonPanel_.classList.add('shaka-tooltips-on'); } @@ -331,6 +334,10 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget { /** @private {shaka.ui.Localization} */ this.localization_ = shaka.ui.Controls.createLocalization_(); + /** @private {?shaka.ui.TextStylePreview} */ + this.textStylePreview_ = new shaka.ui.TextStylePreview( + this.localPlayer_, this.localization_); + /** @private {shaka.util.EventManager} */ this.eventManager_ = new shaka.util.EventManager(); @@ -431,6 +438,9 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget { await document.exitPictureInPicture(); } + this.textStylePreview_?.release(); + this.textStylePreview_ = null; + this.eventManager_?.release(); this.eventManager_ = null; @@ -568,6 +578,7 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget { */ configure(config) { this.config_ = config; + this.hideTextStylePreview(); this.castProxy_.changeReceiverId(config.castReceiverAppId, config.castAndroidReceiverCompatible); @@ -912,6 +923,35 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget { for (const menu of this.contextMenus_) { shaka.ui.Utils.setDisplay(menu, /* visible= */ false); } + this.hideTextStylePreview(); + } + + /** + * Shows a temporary subtitle with the current text displayer style. + */ + showTextStylePreview() { + this.textStylePreview_?.show(); + } + + /** + * Updates the temporary subtitle style without changing player config. + * + * @param {!shaka.ui.TextStylePreview.Configuration=} config + */ + updateTextStylePreview(config = {}) { + this.textStylePreview_?.update(config); + } + + /** + * Reverts the temporary subtitle to the style captured when the menu opened. + */ + resetTextStylePreview() { + this.textStylePreview_?.reset(); + } + + /** Removes the temporary subtitle style preview. */ + hideTextStylePreview() { + this.textStylePreview_?.hide(); } /** @@ -1318,6 +1358,8 @@ shaka.ui.Controls = class extends shaka.util.FakeEventTarget { this.videoContainer_.getElementsByClassName('shaka-settings-menu')); this.menus_.push(...Array.from( this.videoContainer_.getElementsByClassName('shaka-overflow-menu'))); + this.menus_.push(...Array.from( + this.videoContainer_.getElementsByClassName('shaka-sub-menu'))); this.contextMenus_ = Array.from( this.videoContainer_.getElementsByClassName('shaka-context-menu')); diff --git a/ui/locales/ar-XB.json b/ui/locales/ar-XB.json index 98b01d386..532116bea 100644 --- a/ui/locales/ar-XB.json +++ b/ui/locales/ar-XB.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "‏‮Skip‬‏ ‏‮ahead‬‏ ‏‮to‬‏ ‏‮live‬‏", "STATISTICS": "‏‮Statistics‬‏", "SUBTITLE_FORCED": "‏‮Forced‬‏", + "SUBTITLES_EXAMPLE": "‏‮Subtitles‬‏ ‏‮example‬‏", "SURROUND": "‏‮Surround‬‏", "TOGGLE_STEREOSCOPIC": "‏‮Toggle‬‏ ‏‮stereoscopic‬‏", "UNDETERMINED_LANGUAGE": "‏‮Undetermined‬‏", diff --git a/ui/locales/ar.json b/ui/locales/ar.json index ed93832d0..77cdffeb3 100644 --- a/ui/locales/ar.json +++ b/ui/locales/ar.json @@ -41,6 +41,7 @@ "SKIP_TO_LIVE": "الانتقال إلى بث مباشر", "STATISTICS": "الإحصاءات", "SUBTITLE_FORCED": "عرض إجباري", + "SUBTITLES_EXAMPLE": "مثال على الترجمة", "SURROUND": "صوت محيطي", "TOGGLE_STEREOSCOPIC": "إيقاف العرض المجسّم أو تفعيله", "UNDETERMINED_LANGUAGE": "غير محدد", diff --git a/ui/locales/be.json b/ui/locales/be.json index 9a72e1d08..96fb2b806 100644 --- a/ui/locales/be.json +++ b/ui/locales/be.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Прапусціць і перайсці да жывога эфіра", "STATISTICS": "Статыстыка", "SUBTITLE_FORCED": "Субцітры прымусова", + "SUBTITLES_EXAMPLE": "Прыклад субцітраў", "SURROUND": "Аб'ёмны гук", "TOGGLE_STEREOSCOPIC": "Уключыць або адключыць стэрэаскапічны рэжым", "UNDETERMINED_LANGUAGE": "Не пазначана", diff --git a/ui/locales/bg.json b/ui/locales/bg.json index 398fa8260..a41f6a3eb 100644 --- a/ui/locales/bg.json +++ b/ui/locales/bg.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Преминаване напред към предаването на живо", "STATISTICS": "Статистически данни", "SUBTITLE_FORCED": "Принудително", + "SUBTITLES_EXAMPLE": "Примерни субтитри", "SURROUND": "Съраунд", "TOGGLE_STEREOSCOPIC": "Превключване на стереоскопичния режим", "UNDETERMINED_LANGUAGE": "Неопределено", diff --git a/ui/locales/ca.json b/ui/locales/ca.json index 08b8add26..d4d794037 100644 --- a/ui/locales/ca.json +++ b/ui/locales/ca.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Passa a l'emissió en directe", "STATISTICS": "Estadístiques", "SUBTITLE_FORCED": "Forçat", + "SUBTITLES_EXAMPLE": "Exemple de subtítols", "SURROUND": "So envoltant", "TOGGLE_STEREOSCOPIC": "Commuta l'opció estereoscòpica", "UNDETERMINED_LANGUAGE": "Sense especificar", diff --git a/ui/locales/cs.json b/ui/locales/cs.json index 951366938..36b4c6dbe 100644 --- a/ui/locales/cs.json +++ b/ui/locales/cs.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Přeskočit na živé vysílání", "STATISTICS": "Statistiky", "SUBTITLE_FORCED": "Vynuceno", + "SUBTITLES_EXAMPLE": "Ukázka titulků", "SURROUND": "Prostorový zvuk", "TOGGLE_STEREOSCOPIC": "Přepnout stereoskopické zobrazení", "UNDETERMINED_LANGUAGE": "Neurčeno", diff --git a/ui/locales/da.json b/ui/locales/da.json index 5206a700e..efc8333be 100644 --- a/ui/locales/da.json +++ b/ui/locales/da.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Gå videre til liveudsendelse", "STATISTICS": "Statistik", "SUBTITLE_FORCED": "Gennemtvunget", + "SUBTITLES_EXAMPLE": "Eksempel på undertekster", "SURROUND": "Surroundsound", "TOGGLE_STEREOSCOPIC": "Slå stereoskopisk visning til/fra", "UNDETERMINED_LANGUAGE": "Ikke fastslået", diff --git a/ui/locales/de.json b/ui/locales/de.json index add3de7c6..6ad39a975 100644 --- a/ui/locales/de.json +++ b/ui/locales/de.json @@ -50,6 +50,7 @@ "SKIP_TO_LIVE": "Zum Live-Videostream wechseln", "STATISTICS": "Statistiken", "SUBTITLE_FORCED": "Erzwungen", + "SUBTITLES_EXAMPLE": "Beispiel für Untertitel", "SUBTITLE_POSITION": "Untertitelposition", "SURROUND": "Surround", "TOGGLE_STEREOSCOPIC": "Einstellung „stereoskopisch“ ein‑/ausschalten", diff --git a/ui/locales/el.json b/ui/locales/el.json index 0968ba961..6a53b8395 100644 --- a/ui/locales/el.json +++ b/ui/locales/el.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Μετάβαση στη ζωντανή μετάδοση", "STATISTICS": "Στατιστικά στοιχεία", "SUBTITLE_FORCED": "Αναγκαστικό", + "SUBTITLES_EXAMPLE": "Παράδειγμα υπότιτλων", "SURROUND": "Surround", "TOGGLE_STEREOSCOPIC": "Εναλλαγή στερεοσκοπικής", "UNDETERMINED_LANGUAGE": "Δεν έχει καθοριστεί", diff --git a/ui/locales/en-GB.json b/ui/locales/en-GB.json index 1e235156f..20cb46f45 100644 --- a/ui/locales/en-GB.json +++ b/ui/locales/en-GB.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Skip ahead to live", "STATISTICS": "Statistics", "SUBTITLE_FORCED": "Forced", + "SUBTITLES_EXAMPLE": "Subtitles example", "SURROUND": "Surround", "TOGGLE_STEREOSCOPIC": "Toggle stereoscopic", "UNDETERMINED_LANGUAGE": "Undetermined", diff --git a/ui/locales/en-XA.json b/ui/locales/en-XA.json index 144dc0e7b..f5453a77f 100644 --- a/ui/locales/en-XA.json +++ b/ui/locales/en-XA.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "[Šķîþ åĥéåð ţö ļîvé one two three four]", "STATISTICS": "[Šţåţîšţîçš one two]", "SUBTITLE_FORCED": "[Föŕçéð one]", + "SUBTITLES_EXAMPLE": "[Šüƀţîţļéš éẋåɱƥļé one two]", "SURROUND": "[Šûŕŕöûñð one]", "TOGGLE_STEREOSCOPIC": "[Ţöĝĝļé šţéŕéöšçöþîç one two three]", "UNDETERMINED_LANGUAGE": "[Ûñðéţéŕmîñéð one two]", diff --git a/ui/locales/en.json b/ui/locales/en.json index b8ddc1a1a..aadd3347d 100644 --- a/ui/locales/en.json +++ b/ui/locales/en.json @@ -54,6 +54,7 @@ "SKIP_PREVIOUS": "Previous", "STATISTICS": "Statistics", "SUBTITLE_FORCED": "Forced", + "SUBTITLES_EXAMPLE": "Subtitles example", "SUBTITLE_POSITION": "Subtitle position", "SUBTITLE_SIZE": "Subtitle size", "SURROUND": "Surround", diff --git a/ui/locales/es-419.json b/ui/locales/es-419.json index 8b0cd79be..97ac20651 100644 --- a/ui/locales/es-419.json +++ b/ui/locales/es-419.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Adelantar hasta la transmisión en vivo", "STATISTICS": "Estadísticas", "SUBTITLE_FORCED": "Forzado", + "SUBTITLES_EXAMPLE": "Ejemplo de subtítulos", "SURROUND": "Sonido envolvente", "TOGGLE_STEREOSCOPIC": "Activar o desactivar el modo estereoscópico", "UNDETERMINED_LANGUAGE": "Sin especificar", diff --git a/ui/locales/es.json b/ui/locales/es.json index 6a65fdb40..e86ba5be6 100644 --- a/ui/locales/es.json +++ b/ui/locales/es.json @@ -54,6 +54,7 @@ "SKIP_PREVIOUS": "Anterior", "STATISTICS": "Estadísticas", "SUBTITLE_FORCED": "Forzado", + "SUBTITLES_EXAMPLE": "Ejemplo de subtítulos", "SUBTITLE_POSITION": "Posición de los subtítulos", "SUBTITLE_SIZE": "Tamaño de los subtítulos", "SURROUND": "Envolvente", diff --git a/ui/locales/fa.json b/ui/locales/fa.json index 62bd53b7f..e61500324 100644 --- a/ui/locales/fa.json +++ b/ui/locales/fa.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "برای پخش مستقیم رد شوید و به جلو بروید", "STATISTICS": "آمار", "SUBTITLE_FORCED": "نمایش اجباری", + "SUBTITLES_EXAMPLE": "نمونه زیرنویس", "SURROUND": "فراگیر", "TOGGLE_STEREOSCOPIC": "روشن/خاموش کردن برجسته‌نمایی", "UNDETERMINED_LANGUAGE": "نامعین", diff --git a/ui/locales/fi.json b/ui/locales/fi.json index 3bf151ff5..929c81ce7 100644 --- a/ui/locales/fi.json +++ b/ui/locales/fi.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Siirry suoraan lähetykseen", "STATISTICS": "Tilastot", "SUBTITLE_FORCED": "Pakotettu", + "SUBTITLES_EXAMPLE": "Tekstitysesimerkki", "SURROUND": "Surround", "TOGGLE_STEREOSCOPIC": "Stereoskooppi päälle/pois", "UNDETERMINED_LANGUAGE": "Määrittämätön", diff --git a/ui/locales/fil.json b/ui/locales/fil.json index 847e15b96..c15ddfa60 100644 --- a/ui/locales/fil.json +++ b/ui/locales/fil.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Lumaktaw sa live", "STATISTICS": "Mga Istatistika", "SUBTITLE_FORCED": "Sapilitan", + "SUBTITLES_EXAMPLE": "Halimbawa ng subtitle", "SURROUND": "Surround", "TOGGLE_STEREOSCOPIC": "I-toggle sa stereoscopic", "UNDETERMINED_LANGUAGE": "Hindi tinukoy", diff --git a/ui/locales/fr.json b/ui/locales/fr.json index b3150c726..3c3c94516 100644 --- a/ui/locales/fr.json +++ b/ui/locales/fr.json @@ -53,6 +53,7 @@ "SKIP_PREVIOUS": "Précédente", "STATISTICS": "Statistiques", "SUBTITLE_FORCED": "Forcé", + "SUBTITLES_EXAMPLE": "Exemple de sous-titres", "SUBTITLE_POSITION": "Position des sous-titres", "SUBTITLE_SIZE": "Taille des sous-titres", "SURROUND": "Surround", diff --git a/ui/locales/hi.json b/ui/locales/hi.json index c09f8b4b5..a5ae29736 100644 --- a/ui/locales/hi.json +++ b/ui/locales/hi.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "सीधा लाइव प्रसारण देखने के लिए छोड़ें", "STATISTICS": "आंकड़े", "SUBTITLE_FORCED": "फ़ोर्स्ड", + "SUBTITLES_EXAMPLE": "उपशीर्षक उदाहरण", "SURROUND": "सराउंड", "TOGGLE_STEREOSCOPIC": "स्टीरियोस्कोपिक को टॉगल करें", "UNDETERMINED_LANGUAGE": "जानकारी नहीं है", diff --git a/ui/locales/hr.json b/ui/locales/hr.json index 3dc0bb1fb..1bcc2d99a 100644 --- a/ui/locales/hr.json +++ b/ui/locales/hr.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Preskoči na uživo", "STATISTICS": "Statistika", "SUBTITLE_FORCED": "Prisilno", + "SUBTITLES_EXAMPLE": "Primjer titlova", "SURROUND": "Surround", "TOGGLE_STEREOSCOPIC": "Prebaci stereoskopski prikaz", "UNDETERMINED_LANGUAGE": "Neodređeno", diff --git a/ui/locales/hu.json b/ui/locales/hu.json index 2bb505c54..878611288 100644 --- a/ui/locales/hu.json +++ b/ui/locales/hu.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Ugrás az élő közvetítésre", "STATISTICS": "Statisztikák", "SUBTITLE_FORCED": "Kényszerített", + "SUBTITLES_EXAMPLE": "Feliratminta", "SURROUND": "Térhatású hangzás", "TOGGLE_STEREOSCOPIC": "Sztereó térhatás be-/kikapcsolása", "UNDETERMINED_LANGUAGE": "Meghatározatlan", diff --git a/ui/locales/id.json b/ui/locales/id.json index b47677dfb..bcb6cd626 100644 --- a/ui/locales/id.json +++ b/ui/locales/id.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Langsung ke live streaming", "STATISTICS": "Statistik", "SUBTITLE_FORCED": "Dipaksa", + "SUBTITLES_EXAMPLE": "Contoh subtitle", "SURROUND": "Surround", "TOGGLE_STEREOSCOPIC": "Aktifkan/nonaktifkan stereoskop", "UNDETERMINED_LANGUAGE": "Belum ditentukan", diff --git a/ui/locales/it.json b/ui/locales/it.json index 5f0ad5538..2062fda5e 100644 --- a/ui/locales/it.json +++ b/ui/locales/it.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Passa alla trasmissione dal vivo", "STATISTICS": "Statistiche", "SUBTITLE_FORCED": "Forzata", + "SUBTITLES_EXAMPLE": "Esempio di sottotitoli", "SURROUND": "Surround", "TOGGLE_STEREOSCOPIC": "Pulsante di attivazione/disattivazione stereoscopico", "UNDETERMINED_LANGUAGE": "Indeterminata", diff --git a/ui/locales/iw.json b/ui/locales/iw.json index de79190a9..c55e18f2a 100644 --- a/ui/locales/iw.json +++ b/ui/locales/iw.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "דילוג קדימה לשידור הישיר", "STATISTICS": "נתונים סטטיסטיים", "SUBTITLE_FORCED": "מאולץ", + "SUBTITLES_EXAMPLE": "דוגמה לכתוביות", "SURROUND": "סראונד", "TOGGLE_STEREOSCOPIC": "החלפת מצב סטריאוסקופי", "UNDETERMINED_LANGUAGE": "לא ידוע", diff --git a/ui/locales/ja.json b/ui/locales/ja.json index 198dffdf0..cbef61391 100644 --- a/ui/locales/ja.json +++ b/ui/locales/ja.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "ライブ配信までスキップ", "STATISTICS": "統計情報", "SUBTITLE_FORCED": "強制", + "SUBTITLES_EXAMPLE": "字幕の例", "SURROUND": "サラウンド", "TOGGLE_STEREOSCOPIC": "立体画像を切り替える", "UNDETERMINED_LANGUAGE": "不明", diff --git a/ui/locales/ko.json b/ui/locales/ko.json index a4551743a..daf16ce85 100644 --- a/ui/locales/ko.json +++ b/ui/locales/ko.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "라이브 동영상으로 건너뛰기", "STATISTICS": "통계", "SUBTITLE_FORCED": "강제", + "SUBTITLES_EXAMPLE": "자막 예시", "SURROUND": "서라운드", "TOGGLE_STEREOSCOPIC": "입체 보기 전환", "UNDETERMINED_LANGUAGE": "미정", diff --git a/ui/locales/lt.json b/ui/locales/lt.json index 7e1e513f5..649cc988a 100644 --- a/ui/locales/lt.json +++ b/ui/locales/lt.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Praleisti ir transliuoti tiesiogiai", "STATISTICS": "Statistika", "SUBTITLE_FORCED": "Priverstinis", + "SUBTITLES_EXAMPLE": "Subtitrų pavyzdys", "SURROUND": "Erdvinis garsas", "TOGGLE_STEREOSCOPIC": "Perjungti stereoskopinį vaizdą", "UNDETERMINED_LANGUAGE": "Nenustatyta", diff --git a/ui/locales/lv.json b/ui/locales/lv.json index 99002e7be..537568afe 100644 --- a/ui/locales/lv.json +++ b/ui/locales/lv.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Pāriet uz tiešraidi", "STATISTICS": "Statistika", "SUBTITLE_FORCED": "Piespiedu kārtā", + "SUBTITLES_EXAMPLE": "Subtitru piemērs", "SURROUND": "Telpiskā skaņa", "TOGGLE_STEREOSCOPIC": "Pārslēgt stereoskopisko režīmu", "UNDETERMINED_LANGUAGE": "Nenoteikts", diff --git a/ui/locales/nl.json b/ui/locales/nl.json index eb2cb41ed..6159a289b 100644 --- a/ui/locales/nl.json +++ b/ui/locales/nl.json @@ -50,6 +50,7 @@ "SKIP_TO_LIVE": "Doorgaan naar live", "STATISTICS": "Statistieken", "SUBTITLE_FORCED": "Afgedwongen", + "SUBTITLES_EXAMPLE": "Voorbeeld van ondertitels", "SUBTITLE_POSITION": "Positie van ondertitels", "SURROUND": "Surround", "TOGGLE_STEREOSCOPIC": "Stereoscopisch aan-/uitzetten", diff --git a/ui/locales/no.json b/ui/locales/no.json index 2f71055b6..2cbdf3019 100644 --- a/ui/locales/no.json +++ b/ui/locales/no.json @@ -41,6 +41,7 @@ "SKIP_TO_LIVE": "Hopp frem til direktesending", "STATISTICS": "Statistikk", "SUBTITLE_FORCED": "Tvungent", + "SUBTITLES_EXAMPLE": "Eksempel på undertekster", "SURROUND": "Surround-lyd", "TOGGLE_STEREOSCOPIC": "Slå stereoskopisk modus av/på", "UNDETERMINED_LANGUAGE": "Ubestemt", diff --git a/ui/locales/oc.json b/ui/locales/oc.json index 1e5d89f98..60b8f5674 100644 --- a/ui/locales/oc.json +++ b/ui/locales/oc.json @@ -35,8 +35,9 @@ "SKIP_TO_LIVE": "Anar al dirècte", "STATISTICS": "Estatisticas", "SUBTITLE_FORCED": "Forçat", + "SUBTITLES_EXAMPLE": "Exemple de sostítols", "UNDETERMINED_LANGUAGE": "Lenga indeterminada", "UNMUTE": "Desamudir", "UNRECOGNIZED_LANGUAGE": "Non reconeguda", - "VOLUME": "Volum", + "VOLUME": "Volum" } diff --git a/ui/locales/pl.json b/ui/locales/pl.json index 717c8183a..7f418fb06 100644 --- a/ui/locales/pl.json +++ b/ui/locales/pl.json @@ -52,6 +52,7 @@ "SKIP_TO_LIVE": "Przejdź do transmisji na żywo", "STATISTICS": "Statystyki", "SUBTITLE_FORCED": "Wymuszone", + "SUBTITLES_EXAMPLE": "Przykład napisów", "SUBTITLE_POSITION": "Pozycja napisów", "SUBTITLE_SIZE": "Rozmiar napisów", "SURROUND": "Przestrzenny", diff --git a/ui/locales/pt-BR.json b/ui/locales/pt-BR.json index f045bc892..6bebe0b1c 100644 --- a/ui/locales/pt-BR.json +++ b/ui/locales/pt-BR.json @@ -41,6 +41,7 @@ "SKIP_TO_LIVE": "Pular para transmissão ao vivo", "STATISTICS": "Estatísticas", "SUBTITLE_FORCED": "Exibição forçada", + "SUBTITLES_EXAMPLE": "Exemplo de legendas", "SURROUND": "Surround", "TOGGLE_STEREOSCOPIC": "Alternar imagem estereoscópica", "UNDETERMINED_LANGUAGE": "Indeterminado", diff --git a/ui/locales/pt-PT.json b/ui/locales/pt-PT.json index 7095e3cf0..ca13bd591 100644 --- a/ui/locales/pt-PT.json +++ b/ui/locales/pt-PT.json @@ -41,6 +41,7 @@ "SKIP_TO_LIVE": "Avançar para o direto", "STATISTICS": "Estatísticas", "SUBTITLE_FORCED": "Forçada", + "SUBTITLES_EXAMPLE": "Exemplo de legendas", "SURROUND": "Surround", "TOGGLE_STEREOSCOPIC": "Ativar/desativar estereoscópico", "UNDETERMINED_LANGUAGE": "Indeterminado", diff --git a/ui/locales/ro.json b/ui/locales/ro.json index 79888324d..f31fb209f 100644 --- a/ui/locales/ro.json +++ b/ui/locales/ro.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Accesează difuzarea live", "STATISTICS": "Statistici", "SUBTITLE_FORCED": "Forțat", + "SUBTITLES_EXAMPLE": "Exemplu de subtitrări", "SURROUND": "Surround", "TOGGLE_STEREOSCOPIC": "Comută la stereoscopic", "UNDETERMINED_LANGUAGE": "Nestabilită", diff --git a/ui/locales/ru.json b/ui/locales/ru.json index 434eb45da..782f5f120 100644 --- a/ui/locales/ru.json +++ b/ui/locales/ru.json @@ -41,6 +41,7 @@ "SKIP_TO_LIVE": "Пропустить и перейти к прямой трансляции", "STATISTICS": "Статистика", "SUBTITLE_FORCED": "Субтитры принудительно", + "SUBTITLES_EXAMPLE": "Пример субтитров", "SURROUND": "Объемный звук", "TOGGLE_STEREOSCOPIC": "Включить или отключить стереоскопический режим", "UNDETERMINED_LANGUAGE": "Не указано", diff --git a/ui/locales/sjn.json b/ui/locales/sjn.json index c2a48df58..426453aa9 100644 --- a/ui/locales/sjn.json +++ b/ui/locales/sjn.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "  ", "STATISTICS": " ", "SUBTITLE_FORCED": "  ", + "SUBTITLES_EXAMPLE": "Subtitles example", "SURROUND": "", "TOGGLE_STEREOSCOPIC": "", "UNDETERMINED_LANGUAGE": " ", diff --git a/ui/locales/sk.json b/ui/locales/sk.json index cceb21720..a63f432d0 100644 --- a/ui/locales/sk.json +++ b/ui/locales/sk.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Preskočiť na živé vysielanie", "STATISTICS": "Štatistiky", "SUBTITLE_FORCED": "Vynútené", + "SUBTITLES_EXAMPLE": "Príklad titulkov", "SURROUND": "Priestorový zvuk", "TOGGLE_STEREOSCOPIC": "Prepnúť na stereoskopický režim", "UNDETERMINED_LANGUAGE": "Neurčené", diff --git a/ui/locales/sl.json b/ui/locales/sl.json index 11eb6959e..ef477bd0a 100644 --- a/ui/locales/sl.json +++ b/ui/locales/sl.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Preskoči naprej na oddajanje v živo", "STATISTICS": "Statistični podatki", "SUBTITLE_FORCED": "Vsiljeno", + "SUBTITLES_EXAMPLE": "Primer podnapisov", "SURROUND": "Prostorski zvok", "TOGGLE_STEREOSCOPIC": "Preklop stereoskopskega načina", "UNDETERMINED_LANGUAGE": "Nedoločen", diff --git a/ui/locales/source.json b/ui/locales/source.json index e8ae0ace9..df2953e9c 100644 --- a/ui/locales/source.json +++ b/ui/locales/source.json @@ -223,6 +223,10 @@ "description": "Label used to identify a subtitle track that is forced to be shown.", "message": "Forced" }, + "SUBTITLES_EXAMPLE": { + "description": "Example subtitle text shown while previewing subtitle style settings when no subtitle is currently visible.", + "message": "Subtitles example" + }, "SUBTITLE_POSITION": { "description": "Label for a button used to indicate a subtitle position.", "message": "Subtitle position" diff --git a/ui/locales/sr.json b/ui/locales/sr.json index e8d624e92..0a872adf8 100644 --- a/ui/locales/sr.json +++ b/ui/locales/sr.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Прескочи на емитовање уживо", "STATISTICS": "Статистика", "SUBTITLE_FORCED": "Принудно", + "SUBTITLES_EXAMPLE": "Пример титлов", "SURROUND": "Просторни звук", "TOGGLE_STEREOSCOPIC": "Укључи/искључи стерео", "UNDETERMINED_LANGUAGE": "Неодређено", diff --git a/ui/locales/sv.json b/ui/locales/sv.json index 5381e5ff2..6fba18b8d 100644 --- a/ui/locales/sv.json +++ b/ui/locales/sv.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Fortsätt direkt till direktsändning", "STATISTICS": "Statistik", "SUBTITLE_FORCED": "Framtvingad", + "SUBTITLES_EXAMPLE": "Exempel på undertexter", "SURROUND": "Surroundljud", "TOGGLE_STEREOSCOPIC": "Aktivera/inaktivera stereoskopiska bilder", "UNDETERMINED_LANGUAGE": "Obestämt", diff --git a/ui/locales/th.json b/ui/locales/th.json index 383eee48b..de174f8c9 100644 --- a/ui/locales/th.json +++ b/ui/locales/th.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "ข้ามไปที่การถ่ายทอดสด", "STATISTICS": "สถิติ", "SUBTITLE_FORCED": "บังคับ", + "SUBTITLES_EXAMPLE": "ตัวอย่างคำบรรยาย", "SURROUND": "เซอร์ราวด์", "TOGGLE_STEREOSCOPIC": "เปิด/ปิดฟีเจอร์สามมิติ", "UNDETERMINED_LANGUAGE": "ไม่กำหนด", diff --git a/ui/locales/tr.json b/ui/locales/tr.json index 6a5206d3c..cc7c89c52 100644 --- a/ui/locales/tr.json +++ b/ui/locales/tr.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Canlı yayına atla", "STATISTICS": "İstatistikler", "SUBTITLE_FORCED": "Zorunlu", + "SUBTITLES_EXAMPLE": "Altyazı örneği", "SURROUND": "Surround", "TOGGLE_STEREOSCOPIC": "Stereoskopik modu aç/kapat", "UNDETERMINED_LANGUAGE": "Belirsiz", diff --git a/ui/locales/uk.json b/ui/locales/uk.json index da227e527..7b699bb32 100644 --- a/ui/locales/uk.json +++ b/ui/locales/uk.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Перейти до прямої трансляції", "STATISTICS": "Статистика", "SUBTITLE_FORCED": "Обов’язкові субтитри", + "SUBTITLES_EXAMPLE": "Приклад субтитрів", "SURROUND": "Об’ємний звук", "TOGGLE_STEREOSCOPIC": "Увімкнути або вимкнути стереоскопічний режим", "UNDETERMINED_LANGUAGE": "Не визначено", diff --git a/ui/locales/vi.json b/ui/locales/vi.json index 8723884a1..423baea56 100644 --- a/ui/locales/vi.json +++ b/ui/locales/vi.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "Tua tới chương trình phát trực tiếp", "STATISTICS": "Số liệu thống kê", "SUBTITLE_FORCED": "Buộc hiển thị", + "SUBTITLES_EXAMPLE": "Ví dụ phụ đề", "SURROUND": "Vòm", "TOGGLE_STEREOSCOPIC": "Bật/tắt chế độ hình nổi", "UNDETERMINED_LANGUAGE": "Chưa xác định", diff --git a/ui/locales/zh-HK.json b/ui/locales/zh-HK.json index 1575b4237..1367d65c3 100644 --- a/ui/locales/zh-HK.json +++ b/ui/locales/zh-HK.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "跳至当前直播", "STATISTICS": "統計資料", "SUBTITLE_FORCED": "強制", + "SUBTITLES_EXAMPLE": "字幕範例", "SURROUND": "環迴音效", "TOGGLE_STEREOSCOPIC": "切換立體視覺", "UNDETERMINED_LANGUAGE": "不明", diff --git a/ui/locales/zh-TW.json b/ui/locales/zh-TW.json index d06338863..9b1fdb548 100644 --- a/ui/locales/zh-TW.json +++ b/ui/locales/zh-TW.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "跳至当前直播", "STATISTICS": "統計資料", "SUBTITLE_FORCED": "強制顯示", + "SUBTITLES_EXAMPLE": "字幕範例", "SURROUND": "環場音效", "TOGGLE_STEREOSCOPIC": "切換立體影像", "UNDETERMINED_LANGUAGE": "不明", diff --git a/ui/locales/zh.json b/ui/locales/zh.json index d0a4d3a9f..0bfad4cf4 100644 --- a/ui/locales/zh.json +++ b/ui/locales/zh.json @@ -40,6 +40,7 @@ "SKIP_TO_LIVE": "跳至当前直播", "STATISTICS": "统计信息", "SUBTITLE_FORCED": "已强制显示", + "SUBTITLES_EXAMPLE": "字幕示例", "SURROUND": "环绕声", "TOGGLE_STEREOSCOPIC": "切换立体声", "UNDETERMINED_LANGUAGE": "未确定", diff --git a/ui/settings_menu.js b/ui/settings_menu.js index d481e7c74..7897f726d 100644 --- a/ui/settings_menu.js +++ b/ui/settings_menu.js @@ -40,6 +40,9 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { this.addMenu_(); + /** @private {boolean} */ + this.isMenuOpened_ = false; + this.inOverflowMenu_(); this.eventManager.listen(this.button, 'click', () => { @@ -55,6 +58,23 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { /** @private {MutationObserver} */ this.mutationObserver_ = null; + /** @private {MutationObserver} */ + this.menuMutationObserver_ = null; + + if (window.MutationObserver) { + this.menuMutationObserver_ = new MutationObserver(() => { + if (this.menu.classList.contains('shaka-hidden')) { + this.notifyMenuClose_(); + } else { + this.notifyMenuOpen_(); + } + }); + this.menuMutationObserver_.observe(this.menu, { + attributes: true, + attributeFilter: ['class'], + }); + } + const resize = () => this.adjustCustomStyle_(); // Use ResizeObserver if available, fallback to window resize event @@ -77,6 +97,10 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { this.mutationObserver_.disconnect(); this.mutationObserver_ = null; } + if (this.menuMutationObserver_) { + this.menuMutationObserver_.disconnect(); + this.menuMutationObserver_ = null; + } super.release(); } @@ -162,6 +186,7 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { this.backIcon_.use(shaka.ui.Enums.MaterialDesignSVGIcons['BACK']); this.eventManager.listen(this.menu, 'click', () => { + this.notifyMenuClose_(); this.controls.dispatchEvent(new shaka.util.FakeEvent('submenuclose')); shaka.ui.Utils.setDisplay(this.menu, false); shaka.ui.Utils.setDisplay(this.parent, true); @@ -177,6 +202,9 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { if (m.type === 'attributes' && m.attributeName === 'class') { const newHidden = this.parent.classList.contains('shaka-hidden'); if (newHidden && prevHidden != newHidden) { + if (!this.menu.classList.contains('shaka-hidden')) { + this.notifyMenuClose_(); + } this.controls.dispatchEvent( new shaka.util.FakeEvent('submenuclose')); shaka.ui.Utils.setDisplay(this.menu, false); @@ -209,10 +237,12 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { this.controls.dispatchEvent(new shaka.util.FakeEvent('submenuopen')); } shaka.ui.Utils.setDisplay(this.menu, true); + this.notifyMenuOpen_(); shaka.ui.Utils.focusOnTheChosenItem(this.menu); this.adjustCustomStyle_(); this.button.setAttribute('aria-expanded', 'true'); } else { + this.notifyMenuClose_(); shaka.ui.Utils.setDisplay(this.menu, false); this.button.setAttribute('aria-expanded', 'false'); this.button.focus(); @@ -220,6 +250,31 @@ shaka.ui.SettingsMenu = class extends shaka.ui.Element { } } + /** @private */ + notifyMenuOpen_() { + if (this.isMenuOpened_) { + return; + } + + this.isMenuOpened_ = true; + this.onMenuOpen(); + } + + /** @private */ + notifyMenuClose_() { + if (!this.isMenuOpened_) { + return; + } + + this.isMenuOpened_ = false; + this.onMenuClose(); + } + + /** @protected */ + onMenuOpen() {} + + /** @protected */ + onMenuClose() {} /** * @private diff --git a/ui/text_position.js b/ui/text_position.js index 508409992..b5ed0f4c3 100644 --- a/ui/text_position.js +++ b/ui/text_position.js @@ -135,12 +135,28 @@ shaka.ui.TextPosition = class extends shaka.ui.SettingsMenu { this.updateTextPositionSelection_(); }); + const previewConfig = {'positionArea': position}; + shaka.ui.Utils.addHoverAndFocusListeners( + this.eventManager, button, + () => this.controls.updateTextStylePreview(previewConfig), + () => this.controls.resetTextStylePreview()); + this.menu.appendChild(button); } this.updateTextPositionSelection_(); shaka.ui.Utils.focusOnTheChosenItem(this.menu); } + /** @override */ + onMenuOpen() { + this.controls.showTextStylePreview(); + } + + /** @override */ + onMenuClose() { + this.controls.hideTextStylePreview(); + } + /** @private */ updateTextPositionSelection_() { // Remove the old checkmark icon and related tags and classes if it exists. diff --git a/ui/text_size.js b/ui/text_size.js index 482fb55a1..9b8494fac 100644 --- a/ui/text_size.js +++ b/ui/text_size.js @@ -133,12 +133,28 @@ shaka.ui.TextSize = class extends shaka.ui.SettingsMenu { this.updateTextSizeSelection_(); }); + const previewConfig = {'fontScaleFactor': fontScaleFactor}; + shaka.ui.Utils.addHoverAndFocusListeners( + this.eventManager, button, + () => this.controls.updateTextStylePreview(previewConfig), + () => this.controls.resetTextStylePreview()); + this.menu.appendChild(button); } this.updateTextSizeSelection_(); shaka.ui.Utils.focusOnTheChosenItem(this.menu); } + /** @override */ + onMenuOpen() { + this.controls.showTextStylePreview(); + } + + /** @override */ + onMenuClose() { + this.controls.hideTextStylePreview(); + } + /** @private */ updateTextSizeSelection_() { // Remove the old checkmark icon and related tags and classes if it exists. diff --git a/ui/text_style_preview.js b/ui/text_style_preview.js new file mode 100644 index 000000000..d6d95e545 --- /dev/null +++ b/ui/text_style_preview.js @@ -0,0 +1,198 @@ +/*! @license + * Shaka Player + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + + +goog.provide('shaka.ui.TextStylePreview'); + +goog.require('shaka.ui.Locales'); +goog.require('shaka.ui.Localization'); +goog.require('shaka.util.EventManager'); +goog.requireType('shaka.config.PositionArea'); +goog.requireType('shaka.Player'); + + +/** + * Manages the temporary subtitle style preview shown while subtitle style + * settings are hovered or focused. + * + * @final + */ +shaka.ui.TextStylePreview = class { + /** + * @param {!shaka.Player} player + * @param {!shaka.ui.Localization} localization + */ + constructor(player, localization) { + /** @private {?shaka.Player} */ + this.player_ = player; + + /** @private {?shaka.ui.Localization} */ + this.localization_ = localization; + + /** @private {?shaka.extern.TextDisplayerConfiguration} */ + this.baseConfig_ = null; + + /** @private {!shaka.ui.TextStylePreview.Configuration} */ + this.previewConfig_ = {}; + + /** @private {boolean} */ + this.shown_ = false; + + /** @private {?shaka.util.EventManager} */ + this.eventManager_ = new shaka.util.EventManager(); + + this.eventManager_.listen(player, 'configurationchanged', () => { + this.updateBaseConfig_(); + }); + + this.eventManager_.listenMulti( + localization, + [ + shaka.ui.Localization.LOCALE_UPDATED, + shaka.ui.Localization.LOCALE_CHANGED, + ], () => { + this.apply_(); + }); + } + + /** Releases all resources owned by this preview. */ + release() { + this.hide(); + this.eventManager_?.release(); + this.eventManager_ = null; + this.player_ = null; + this.localization_ = null; + } + + /** Shows a temporary subtitle with the current text displayer style. */ + show() { + if (!this.player_) { + return; + } + + this.shown_ = true; + this.previewConfig_ = {}; + this.updateBaseConfig_(); + } + + /** + * Updates the temporary subtitle style without changing player config. + * + * @param {!shaka.ui.TextStylePreview.Configuration=} config + */ + update(config = {}) { + if (!this.player_) { + return; + } + + if (!this.shown_) { + this.show(); + } + + this.previewConfig_ = Object.assign({}, config); + this.apply_(); + } + + /** Reverts the temporary subtitle to the style captured when shown. */ + reset() { + if (!this.shown_) { + return; + } + + this.previewConfig_ = {}; + this.apply_(); + } + + /** Removes the temporary subtitle style preview. */ + hide() { + if (!this.shown_) { + return; + } + + this.shown_ = false; + this.baseConfig_ = null; + this.previewConfig_ = {}; + const displayer = this.getTextDisplayer_(); + if (displayer && + typeof displayer['clearTextStylePreview'] == 'function') { + displayer['clearTextStylePreview'](); + } + } + + /** @private */ + updateBaseConfig_() { + if (!this.shown_ || !this.player_) { + return; + } + + this.baseConfig_ = this.getCurrentTextDisplayerConfig_(); + this.apply_(); + } + + /** + * @return {!shaka.extern.TextDisplayerConfiguration} + * @private + */ + getCurrentTextDisplayerConfig_() { + const player = /** @type {!shaka.Player} */(this.player_); + return /** @type {!shaka.extern.TextDisplayerConfiguration} */( + Object.assign({}, player.getConfiguration().textDisplayer)); + } + + /** @private */ + apply_() { + if (!this.shown_ || !this.player_ || !this.baseConfig_) { + return; + } + + const previewConfig = + /** @type {!shaka.extern.TextDisplayerConfiguration} */( + Object.assign({}, this.baseConfig_, this.previewConfig_)); + const displayer = this.getTextDisplayer_(); + if (displayer && typeof displayer['setTextStylePreview'] == 'function') { + displayer['setTextStylePreview']( + previewConfig, this.getLocalizedExampleText_()); + } + } + + /** + * @return {?} + * @private + */ + getTextDisplayer_() { + const player = /** @type {?} */(this.player_); + if (!player || typeof player.getTextDisplayer != 'function') { + return null; + } + + return player.getTextDisplayer(); + } + + /** + * @return {string} + * @private + */ + getLocalizedExampleText_() { + if (!this.localization_) { + return ''; + } + + return this.localization_.resolve( + shaka.ui.Locales.Ids.SUBTITLES_EXAMPLE); + } +}; + + +/** + * @typedef {{ + * fontScaleFactor: (number|undefined), + * positionArea: (shaka.config.PositionArea|undefined), + * }} + * + * @description + * Text displayer fields that the style preview can temporarily override. + */ +shaka.ui.TextStylePreview.Configuration; diff --git a/ui/ui_utils.js b/ui/ui_utils.js index e1b9b7293..320a8dad7 100644 --- a/ui/ui_utils.js +++ b/ui/ui_utils.js @@ -11,6 +11,7 @@ goog.require('goog.asserts'); goog.require('shaka.ui.Enums'); goog.require('shaka.ui.Icon'); goog.require('shaka.util.Mp4Parser'); +goog.requireType('shaka.util.EventManager'); shaka.ui.Utils = class { @@ -96,6 +97,20 @@ shaka.ui.Utils = class { } } + /** + * @param {!shaka.util.EventManager} eventManager + * @param {!Element} element + * @param {function()} onHoverOrFocus + * @param {function()} onLeaveOrBlur + */ + static addHoverAndFocusListeners( + eventManager, element, onHoverOrFocus, onLeaveOrBlur) { + eventManager.listen(element, 'mouseenter', onHoverOrFocus); + eventManager.listen(element, 'mouseleave', onLeaveOrBlur); + eventManager.listen(element, 'focus', onHoverOrFocus); + eventManager.listen(element, 'blur', onLeaveOrBlur); + } + /** * Builds a time string, e.g., 01:04:23, from |displayTime|.