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
if (this.qualitySubmenu) this.qualitySubmenu.style.display = 'none';
if (this.subtitlesSubmenu) this.subtitlesSubmenu.style.display = 'none';
const btnEl = this.settingsButton?.el();
if (btnEl) {
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) {
// Update player speed
this.player().playbackRate(speed);
@ -1062,6 +1085,12 @@ class CustomSettingsMenu extends Component {
this.userPreferences.setPreference('subtitleLanguage', lang || null, true);
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
this.subtitlesSubmenu.querySelectorAll('.subtitle-option').forEach((opt) => {
opt.classList.remove('active');
@ -1078,8 +1107,16 @@ class CustomSettingsMenu extends Component {
const currentSubtitlesDisplay = this.settingsOverlay.querySelector('.current-subtitles');
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';
// Remove active state from settings button
const btnEl = this.settingsButton?.el();
if (btnEl) {
btnEl.classList.remove('settings-clicked');
}
}
restoreSubtitlePreference() {

View File

@ -3,7 +3,7 @@ import videojs from 'video.js';
import 'video.js/dist/video-js.css';
// import '../../VideoJS.css';
import '../../styles/embed.css';
// import '../controls/SubtitlesButton.css';
import '../controls/SubtitlesButton.css';
import './VideoJSPlayer.css';
import './VideoJSPlayerRoundedCorners.css';
import '../controls/ButtonTooltips.css';
@ -42,8 +42,8 @@ const enableStandardButtonTooltips = (player) => {
// volumePanel: 'Volume', // Removed - no tooltip for volume
fullscreenToggle: () => (player.isFullscreen() ? 'Exit fullscreen' : 'Fullscreen'),
pictureInPictureToggle: 'Picture-in-picture',
subtitlesButton: 'Subtitles/CC',
captionsButton: '',
subtitlesButton: '',
captionsButton: 'Captions',
subsCapsButton: '',
chaptersButton: 'Chapters',
audioTrackButton: 'Audio tracks',
@ -1176,7 +1176,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
media_type: 'video',
original_media_url:
'/media/original/user/markos/db52140de7204022a1e5f08e078b4ec6.UniversityofCopenhagenMærskTower.mp4',
_hls_info: {
hls_info: {
master_file: '/media/hls/5073e97457004961a163c5b504e2d7e8/master.m3u8',
'240_iframe': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-1/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',
'360_playlist': '/media/hls/5073e97457004961a163c5b504e2d7e8/media-5/stream.m3u8',
},
hls_info: {},
// hls_info: {},
/* hls_info: {
master_file: '/media/hls/c1ab03cab3bb46b5854a5e217cfe3013/master.m3u8',
'240_iframe': '/media/hls/c1ab03cab3bb46b5854a5e217cfe3013/media-1/iframes.m3u8',
@ -2116,7 +2116,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
descriptionsButton: false,
// 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)
captionsButton: hasSubtitles ? true : false,
@ -2989,7 +2989,7 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
// END: Implement autoplay toggle button
// Make menus clickable instead of hover-only
/* setTimeout(() => {
setTimeout(() => {
const setupClickableMenus = () => {
// Find all menu buttons (subtitles, etc.) - exclude chaptersButton as it has custom overlay
const menuButtons = ['subtitlesButton', 'playbackRateMenuButton'];
@ -3049,54 +3049,149 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
const menu = el.querySelector('.vjs-menu');
if (menu) menu.style.display = 'none';
const toggleSubs = (ev) => {
ev.preventDefault();
ev.stopPropagation();
const tracks = playerRef.current.textTracks();
let any = false;
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
if (t.kind === 'subtitles' && t.mode === 'showing') {
any = true;
break;
// 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();
}
}
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++) {
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');
// Do not change saved language on quick toggle off; save enabled=false
try {
userPreferences.current.setPreference('subtitleEnabled', 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;
if (any) {
for (let i = 0; i < tracks.length; i++) {
const t = tracks[i];
if (t.kind === 'subtitles') t.mode = 'disabled';
}
el.classList.remove('vjs-subs-active');
// Do not change saved language on quick toggle off; save enabled=false
try {
userPreferences.current.setPreference(
'subtitleEnabled',
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++) {
const t = tracks[i];
if (t.kind === 'subtitles') {
first = t.language;
break;
const show = t.language === preferred;
t.mode = show ? 'showing' : 'disabled';
if (show) found = true;
}
}
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) {}
if (found) {
el.classList.add('vjs-subs-active');
try {
userPreferences.current.setPreference(
'subtitleEnabled',
@ -3104,44 +3199,23 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
true
);
} 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
el.addEventListener(
'touchend',
(e) => {
e.preventDefault();
e.stopPropagation();
toggleSubs(e);
},
{ passive: false }
);
// Add mobile touch support for subtitles button
el.addEventListener(
'touchend',
(e) => {
e.preventDefault();
e.stopPropagation();
toggleSubs(e);
},
{ passive: false }
);
}
// Sync underline state on external changes
playerRef.current.on('texttrackchange', () => {
@ -3175,8 +3249,8 @@ function VideoJSPlayer({ videoId = 'default-video' }) {
}
};
// setupClickableMenus();
}, 1500); */
setupClickableMenus();
}, 1500);
// BEGIN: Add chapter markers and sprite preview to progress control
if (progressControl && seekBar) {