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,54 +3049,149 @@ 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';
const toggleSubs = (ev) => { // Different behavior for subtitles button - open settings menu directly
ev.preventDefault(); if (n === 'subtitlesButton') {
ev.stopPropagation(); // Subtitles button opens settings menu directly to subtitles
const tracks = playerRef.current.textTracks(); const openSubtitlesSettings = (ev) => {
let any = false; ev.preventDefault();
for (let i = 0; i < tracks.length; i++) { ev.stopPropagation();
const t = tracks[i];
if (t.kind === 'subtitles' && t.mode === 'showing') { // Open settings menu directly to subtitles submenu
any = true; if (
break; customComponents.current.settingsMenu &&
customComponents.current.settingsMenu.openSubtitlesMenu
) {
customComponents.current.settingsMenu.openSubtitlesMenu();
} }
} };
if (any) {
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) => {
ev.preventDefault();
ev.stopPropagation();
const tracks = playerRef.current.textTracks();
let any = false;
for (let i = 0; i < tracks.length; i++) { for (let i = 0; i < tracks.length; i++) {
const t = tracks[i]; const t = tracks[i];
if (t.kind === 'subtitles') t.mode = 'disabled'; if (t.kind === 'subtitles' && t.mode === 'showing') {
any = true;
break;
}
} }
el.classList.remove('vjs-subs-active'); if (any) {
// Do not change saved language on quick toggle off; save enabled=false for (let i = 0; i < tracks.length; i++) {
try { const t = tracks[i];
userPreferences.current.setPreference('subtitleEnabled', false, true); if (t.kind === 'subtitles') t.mode = 'disabled';
} catch (e) {} }
} else { el.classList.remove('vjs-subs-active');
// Show using previously chosen language only; do not change it // Do not change saved language on quick toggle off; save enabled=false
const preferred = userPreferences.current.getPreference('subtitleLanguage'); try {
if (!preferred) { userPreferences.current.setPreference(
// If no language chosen yet, enable first available and save it 'subtitleEnabled',
let first = null; false,
true
);
} catch (e) {}
} else {
// Show using previously chosen language only; do not change it
const preferred =
userPreferences.current.getPreference('subtitleLanguage');
if (!preferred) {
// If no language chosen yet, enable first available and save it
let first = null;
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
if (t.kind === 'subtitles') {
first = t.language;
break;
}
}
if (first) {
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
if (t.kind === 'subtitles')
t.mode = t.language === first ? 'showing' : 'disabled';
}
try {
userPreferences.current.setPreference(
'subtitleLanguage',
first,
true
);
} catch (e) {}
try {
userPreferences.current.setPreference(
'subtitleEnabled',
true,
true
);
} catch (e) {}
el.classList.add('vjs-subs-active');
}
return;
}
let found = false;
for (let i = 0; i < tracks.length; i++) { for (let i = 0; i < tracks.length; i++) {
const t = tracks[i]; const t = tracks[i];
if (t.kind === 'subtitles') { if (t.kind === 'subtitles') {
first = t.language; const show = t.language === preferred;
break; t.mode = show ? 'showing' : 'disabled';
if (show) found = true;
} }
} }
if (first) { if (found) {
for (let i = 0; i < tracks.length; i++) { el.classList.add('vjs-subs-active');
const t = tracks[i];
if (t.kind === 'subtitles')
t.mode = t.language === first ? 'showing' : 'disabled';
}
try {
userPreferences.current.setPreference(
'subtitleLanguage',
first,
true
);
} catch (e) {}
try { try {
userPreferences.current.setPreference( userPreferences.current.setPreference(
'subtitleEnabled', 'subtitleEnabled',
@ -3104,44 +3199,23 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
true true
); );
} catch (e) {} } catch (e) {}
el.classList.add('vjs-subs-active');
}
return;
}
let found = false;
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
if (t.kind === 'subtitles') {
const show = t.language === preferred;
t.mode = show ? 'showing' : 'disabled';
if (show) found = true;
} }
} }
if (found) { };
el.classList.add('vjs-subs-active');
try {
userPreferences.current.setPreference(
'subtitleEnabled',
true,
true
);
} catch (e) {}
}
}
};
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) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
toggleSubs(e); toggleSubs(e);
}, },
{ 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) {