Improve subtitles menu and button behavior in VideoJS

Adds direct access to the subtitles submenu from the subtitles button, updates the button's active state based on subtitle selection, and dispatches a custom event on subtitle state changes. Also cleans up unused static video.js files and enables the SubtitlesButton CSS.
This commit is contained in:
Yiannis Christodoulou 2025-10-10 11:06:13 +03:00
parent c4551c4050
commit 76e85d2577
2 changed files with 194 additions and 83 deletions

View File

@ -819,6 +819,7 @@ class CustomSettingsMenu extends Component {
this.speedSubmenu.style.display = 'none'; // Hide submenu when main menu toggles this.speedSubmenu.style.display = 'none'; // Hide submenu when main menu toggles
if (this.qualitySubmenu) this.qualitySubmenu.style.display = 'none'; if (this.qualitySubmenu) this.qualitySubmenu.style.display = 'none';
if (this.subtitlesSubmenu) this.subtitlesSubmenu.style.display = 'none';
const btnEl = this.settingsButton?.el(); const btnEl = this.settingsButton?.el();
if (btnEl) { if (btnEl) {
if (!isVisible) { if (!isVisible) {
@ -829,6 +830,28 @@ class CustomSettingsMenu extends Component {
} }
} }
// Method to open settings directly to subtitles submenu
openSubtitlesMenu() {
// First ensure settings overlay is visible
this.settingsOverlay.classList.add('show');
this.settingsOverlay.style.display = 'block';
// Hide other submenus and show subtitles submenu
this.speedSubmenu.style.display = 'none';
if (this.qualitySubmenu) this.qualitySubmenu.style.display = 'none';
if (this.subtitlesSubmenu) {
this.subtitlesSubmenu.style.display = 'flex';
// Refresh the submenu to ensure it's up to date
this.refreshSubtitlesSubmenu();
}
// Mark settings button as active
const btnEl = this.settingsButton?.el();
if (btnEl) {
btnEl.classList.add('settings-clicked');
}
}
handleSpeedChange(speed, speedOption) { handleSpeedChange(speed, speedOption) {
// Update player speed // Update player speed
this.player().playbackRate(speed); this.player().playbackRate(speed);
@ -1062,6 +1085,12 @@ class CustomSettingsMenu extends Component {
this.userPreferences.setPreference('subtitleLanguage', lang || null, true); this.userPreferences.setPreference('subtitleLanguage', lang || null, true);
this.userPreferences.setPreference('subtitleEnabled', !!lang, true); // for iphones this.userPreferences.setPreference('subtitleEnabled', !!lang, true); // for iphones
// Trigger a custom event to notify other components about subtitle state change
const subtitleChangeEvent = new CustomEvent('subtitleStateChanged', {
detail: { enabled: !!lang, language: lang },
});
window.dispatchEvent(subtitleChangeEvent);
// Update UI selection // Update UI selection
this.subtitlesSubmenu.querySelectorAll('.subtitle-option').forEach((opt) => { this.subtitlesSubmenu.querySelectorAll('.subtitle-option').forEach((opt) => {
opt.classList.remove('active'); opt.classList.remove('active');
@ -1078,8 +1107,16 @@ class CustomSettingsMenu extends Component {
const currentSubtitlesDisplay = this.settingsOverlay.querySelector('.current-subtitles'); const currentSubtitlesDisplay = this.settingsOverlay.querySelector('.current-subtitles');
if (currentSubtitlesDisplay) currentSubtitlesDisplay.textContent = label; if (currentSubtitlesDisplay) currentSubtitlesDisplay.textContent = label;
// Close only the subtitles submenu (keep overlay open) // Close the entire settings overlay after subtitle selection
this.settingsOverlay.classList.remove('show');
this.settingsOverlay.style.display = 'none';
this.subtitlesSubmenu.style.display = 'none'; this.subtitlesSubmenu.style.display = 'none';
// Remove active state from settings button
const btnEl = this.settingsButton?.el();
if (btnEl) {
btnEl.classList.remove('settings-clicked');
}
} }
restoreSubtitlePreference() { restoreSubtitlePreference() {

View File

@ -3,7 +3,7 @@ import videojs from 'video.js';
import 'video.js/dist/video-js.css'; import 'video.js/dist/video-js.css';
// import '../../VideoJS.css'; // import '../../VideoJS.css';
import '../../styles/embed.css'; import '../../styles/embed.css';
// import '../controls/SubtitlesButton.css'; import '../controls/SubtitlesButton.css';
import './VideoJSPlayer.css'; import './VideoJSPlayer.css';
import './VideoJSPlayerRoundedCorners.css'; import './VideoJSPlayerRoundedCorners.css';
import '../controls/ButtonTooltips.css'; import '../controls/ButtonTooltips.css';
@ -42,8 +42,8 @@ const enableStandardButtonTooltips = (player) => {
// volumePanel: 'Volume', // Removed - no tooltip for volume // volumePanel: 'Volume', // Removed - no tooltip for volume
fullscreenToggle: () => (player.isFullscreen() ? 'Exit fullscreen' : 'Fullscreen'), fullscreenToggle: () => (player.isFullscreen() ? 'Exit fullscreen' : 'Fullscreen'),
pictureInPictureToggle: 'Picture-in-picture', pictureInPictureToggle: 'Picture-in-picture',
subtitlesButton: 'Subtitles/CC', subtitlesButton: '',
captionsButton: '', captionsButton: 'Captions',
subsCapsButton: '', subsCapsButton: '',
chaptersButton: 'Chapters', chaptersButton: 'Chapters',
audioTrackButton: 'Audio tracks', audioTrackButton: 'Audio tracks',
@ -1176,7 +1176,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
media_type: 'video', media_type: 'video',
original_media_url: original_media_url:
'/media/original/user/markos/db52140de7204022a1e5f08e078b4ec6.UniversityofCopenhagenMærskTower.mp4', '/media/original/user/markos/db52140de7204022a1e5f08e078b4ec6.UniversityofCopenhagenMærskTower.mp4',
_hls_info: { hls_info: {
master_file: '/media/hls/5073e97457004961a163c5b504e2d7e8/master.m3u8', master_file: '/media/hls/5073e97457004961a163c5b504e2d7e8/master.m3u8',
'240_iframe': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-1/iframes.m3u8', '240_iframe': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-1/iframes.m3u8',
'480_iframe': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-2/iframes.m3u8', '480_iframe': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-2/iframes.m3u8',
@ -1189,7 +1189,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
'144_playlist': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-4/stream.m3u8', '144_playlist': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-4/stream.m3u8',
'360_playlist': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-5/stream.m3u8', '360_playlist': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-5/stream.m3u8',
}, },
hls_info: {}, // hls_info: {},
/* hls_info: { /* hls_info: {
master_file: '/media/hls/c1ab03cab3bb46b5854a5e217cfe3013/master.m3u8', master_file: '/media/hls/c1ab03cab3bb46b5854a5e217cfe3013/master.m3u8',
'240_iframe': '/media/hls/c1ab03cab3bb46b5854a5e217cfe3013/media-1/iframes.m3u8', '240_iframe': '/media/hls/c1ab03cab3bb46b5854a5e217cfe3013/media-1/iframes.m3u8',
@ -2116,7 +2116,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
descriptionsButton: false, descriptionsButton: false,
// Subtitles (CC) button should be visible // Subtitles (CC) button should be visible
subtitlesButton: false, // hasSubtitles && !isTouchDevice ? true : false, subtitlesButton: hasSubtitles ? true : false, // hasSubtitles && !isTouchDevice ? true : false,
// Captions button (keep disabled to avoid duplicate with subtitles) // Captions button (keep disabled to avoid duplicate with subtitles)
captionsButton: hasSubtitles ? true : false, captionsButton: hasSubtitles ? true : false,
@ -2989,7 +2989,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
// END: Implement autoplay toggle button // END: Implement autoplay toggle button
// Make menus clickable instead of hover-only // Make menus clickable instead of hover-only
/* setTimeout(() => { setTimeout(() => {
const setupClickableMenus = () => { const setupClickableMenus = () => {
// Find all menu buttons (subtitles, etc.) - exclude chaptersButton as it has custom overlay // Find all menu buttons (subtitles, etc.) - exclude chaptersButton as it has custom overlay
const menuButtons = ['subtitlesButton', 'playbackRateMenuButton']; const menuButtons = ['subtitlesButton', 'playbackRateMenuButton'];
@ -3049,6 +3049,74 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
const menu = el.querySelector('.vjs-menu'); const menu = el.querySelector('.vjs-menu');
if (menu) menu.style.display = 'none'; if (menu) menu.style.display = 'none';
// Different behavior for subtitles button - open settings menu directly
if (n === 'subtitlesButton') {
// Subtitles button opens settings menu directly to subtitles
const openSubtitlesSettings = (ev) => {
ev.preventDefault();
ev.stopPropagation();
// Open settings menu directly to subtitles submenu
if (
customComponents.current.settingsMenu &&
customComponents.current.settingsMenu.openSubtitlesMenu
) {
customComponents.current.settingsMenu.openSubtitlesMenu();
}
};
el.addEventListener('click', openSubtitlesSettings, { capture: true });
// Add mobile touch support for subtitles button
el.addEventListener(
'touchend',
(e) => {
e.preventDefault();
e.stopPropagation();
openSubtitlesSettings(e);
},
{ passive: false }
);
// Apply red underline based on localStorage subtitleEnabled
const updateSubtitleButtonState = () => {
const subtitleEnabled =
userPreferences.current.getPreference('subtitleEnabled');
if (subtitleEnabled) {
el.classList.add('vjs-subs-active');
} else {
el.classList.remove('vjs-subs-active');
}
};
// Initial state
updateSubtitleButtonState();
// Listen for subtitle changes to update the red underline
playerRef.current.on('texttrackchange', updateSubtitleButtonState);
// Listen for custom subtitle state changes from settings menu
const handleSubtitleStateChange = () => {
updateSubtitleButtonState();
};
window.addEventListener('subtitleStateChanged', handleSubtitleStateChange);
// Also listen for storage changes to update button state
const handleStorageChange = () => {
updateSubtitleButtonState();
};
window.addEventListener('storage', handleStorageChange);
// Clean up event listeners when player is disposed
playerRef.current.on('dispose', () => {
window.removeEventListener(
'subtitleStateChanged',
handleSubtitleStateChange
);
window.removeEventListener('storage', handleStorageChange);
});
} else {
// Other buttons (captions, subsCaps) keep the original toggle behavior
const toggleSubs = (ev) => { const toggleSubs = (ev) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -3069,11 +3137,16 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
el.classList.remove('vjs-subs-active'); el.classList.remove('vjs-subs-active');
// Do not change saved language on quick toggle off; save enabled=false // Do not change saved language on quick toggle off; save enabled=false
try { try {
userPreferences.current.setPreference('subtitleEnabled', false, true); userPreferences.current.setPreference(
'subtitleEnabled',
false,
true
);
} catch (e) {} } catch (e) {}
} else { } else {
// Show using previously chosen language only; do not change it // Show using previously chosen language only; do not change it
const preferred = userPreferences.current.getPreference('subtitleLanguage'); const preferred =
userPreferences.current.getPreference('subtitleLanguage');
if (!preferred) { if (!preferred) {
// If no language chosen yet, enable first available and save it // If no language chosen yet, enable first available and save it
let first = null; let first = null;
@ -3132,7 +3205,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
el.addEventListener('click', toggleSubs, { capture: true }); el.addEventListener('click', toggleSubs, { capture: true });
// Add mobile touch support // Add mobile touch support for subtitles button
el.addEventListener( el.addEventListener(
'touchend', 'touchend',
(e) => { (e) => {
@ -3142,6 +3215,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
}, },
{ passive: false } { passive: false }
); );
}
// Sync underline state on external changes // Sync underline state on external changes
playerRef.current.on('texttrackchange', () => { playerRef.current.on('texttrackchange', () => {
@ -3175,8 +3249,8 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
} }
}; };
// setupClickableMenus(); setupClickableMenus();
}, 1500); */ }, 1500);
// BEGIN: Add chapter markers and sprite preview to progress control // BEGIN: Add chapter markers and sprite preview to progress control
if (progressControl && seekBar) { if (progressControl && seekBar) {