Automatically enables, enhances, and translates subtitles on Coursera. Features include a draggable icon, customizable language selection, and real-time translation using Google Translate.
// ==UserScript== // @name Coursera Auto Subtitle // @namespace https://github.com/htrnguyen/Coursera-Auto-Subtitle // @version 1.0 // @description Automatically enables, enhances, and translates subtitles on Coursera. Features include a draggable icon, customizable language selection, and real-time translation using Google Translate. // @author Hà Trọng Nguyễn (htrnguyen) // @match https://www.coursera.org/learn/* // @grant GM_xmlhttpRequest // @connect translate.googleapis.com // @license MIT // @icon https://github.com/htrnguyen/Coursera-Auto-Subtitle/raw/main/coursera-auto-subtitle-logo.png // ==/UserScript== (function () { 'use strict'; const CONFIG = { translat###btitles: true, maxRetries: 3, retryDelay: 1000, }; let isSubtitlesEnabled = false; let subtitleDisplayElement = null; let targetLanguage = 'en'; // Default language is English const LANGUAGES = { vi: 'Tiếng Việt', en: 'English', zh: '中文 (简体)', ja: '日本語', ko: '한국어', fr: 'Français', }; let icon, menu; function createDraggableIcon() { icon = document.createElement('img'); icon.src = 'https://github.com/htrnguyen/Coursera-Auto-Subtitle/raw/main/coursera-auto-subtitle-logo.png'; icon.style.position = 'fixed'; icon.style.top = '20px'; icon.style.left = '20px'; icon.style.zIndex = '9999'; icon.style.cursor = 'pointer'; icon.style.width = '32px'; icon.style.height = '32px'; icon.style.userSelect = 'none'; let isDragging = false; let offsetX, offsetY; icon.addEventListener('mousedown', (event) => { isDragging = true; offsetX = event.clientX - icon.getBoundingClientRect().left; offsetY = event.clientY - icon.getBoundingClientRect().top; icon.style.cursor = 'grabbing'; }); document.addEventListener('mousemove', (event) => { if (isDragging) { const newLeft = event.clientX - offsetX; const newTop = event.clientY - offsetY; // Giới hạn icon trong phạm vi màn hình icon.style.left = `${Math.max(0, Math.min(window.innerWidth - icon.offsetWidth, newLeft))}px`; icon.style.top = `${Math.max(0, Math.min(window.innerHeight - icon.offsetHeight, newTop))}px`; if (menu) { updateMenuPosition(); } } }); document.addEventListener('mouseup', () => { if (isDragging) { isDragging = false; icon.style.cursor = 'pointer'; } }); icon.addEventListener('click', (event) => { event.stopPropagation(); showMenu(); }); document.body.appendChild(icon); } function updateMenuPosition() { const iconRect = icon.getBoundingClientRect(); const menuWidth = 180; // Chiều rộng menu const menuHeight = 120; // Chiều cao menu (ước lượng) // Kiểm tra vị trí icon để hiển thị menu phù hợp if (iconRect.left + icon.offsetWidth + menuWidth > window.innerWidth) { // Icon ở viền phải, hiển thị menu bên trái menu.style.left = `${iconRect.left - menuWidth}px`; menu.style.top = `${iconRect.top}px`; } else if (iconRect.top + icon.offsetHeight + menuHeight > window.innerHeight) { // Icon ở viền dưới, hiển thị menu bên trên menu.style.left = `${iconRect.left + icon.offsetWidth}px`; menu.style.top = `${iconRect.top - menuHeight}px`; } else if (iconRect.top - menuHeight < 0) { // Icon ở viền trên, hiển thị menu bên dưới menu.style.left = `${iconRect.left + icon.offsetWidth}px`; menu.style.top = `${iconRect.top + icon.offsetHeight}px`; } else { // Mặc định hiển thị menu bên phải menu.style.left = `${iconRect.left + icon.offsetWidth}px`; menu.style.top = `${iconRect.top}px`; } } function showMenu() { if (menu) { menu.remove(); menu = null; return; } menu = document.createElement('div'); menu.classList.add('subtitle-menu'); menu.style.position = 'fixed'; menu.style.backgroundColor = 'white'; menu.style.border = '1px solid #ccc'; menu.style.borderRadius = '5px'; menu.style.padding = '10px'; menu.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.1)'; menu.style.zIndex = '10000'; menu.style.width = '180px'; updateMenuPosition(); const toggleButton = document.createElement('button'); toggleButton.textContent = isSubtitlesEnabled ? 'Disable Subtitles' : 'Enable Subtitles'; toggleButton.style.display = 'block'; toggleButton.style.width = '100%'; toggleButton.style.marginBottom = '10px'; toggleButton.style.padding = '8px'; toggleButton.style.cursor = 'pointer'; toggleButton.style.fontSize = '14px'; toggleButton.addEventListener('click', (event) => { event.stopPropagation(); isSubtitlesEnabled = !isSubtitlesEnabled; toggleButton.textContent = isSubtitlesEnabled ? 'Disable Subtitles' : 'Enable Subtitles'; if (isSubtitlesEnabled) { enabl###btitles(); } else { disabl###btitles(); } menu.remove(); menu = null; }); const languageSelect = document.createElement('select'); languageSelect.style.display = 'block'; languageSelect.style.width = '100%'; languageSelect.style.padding = '8px'; languageSelect.style.cursor = 'pointer'; languageSelect.style.fontSize = '14px'; for (const [code, name] of Object.entries(LANGUAGES)) { const option = document.createElement('option'); option.value = code; option.textContent = name; if (code === targetLanguage) option.selected = true; languageSelect.appendChild(option); } languageSelect.addEventListener('click', (event) => { event.stopPropagation(); }); languageSelect.addEventListener('change', (event) => { event.stopPropagation(); targetLanguage = event.target.value; if (isSubtitlesEnabled) { handl###btitles(); } }); menu.appendChild(toggleButton); menu.appendChild(languageSelect); document.body.appendChild(menu); document.addEventListener('click', (event) => { if (!menu.contains(event.target) && !icon.contains(event.target)) { menu.remove(); menu = null; } }); } function enabl###btitles() { if (!subtitleDisplayElement) { creat###btitleDisplay(); } subtitleDisplayElement.style.display = 'block'; handl###btitles(); } function disabl###btitles() { if (subtitleDisplayElement) { subtitleDisplayElement.style.display = 'none'; } } function creat###btitleDisplay() { subtitleDisplayElement = document.createElement('div'); subtitleDisplayElement.style.position = 'absolute'; subtitleDisplayElement.style.bottom = '20px'; subtitleDisplayElement.style.left = '50%'; subtitleDisplayElement.style.transform = 'translateX(-50%)'; subtitleDisplayElement.style.color = 'white'; subtitleDisplayElement.style.fontSize = '16px'; subtitleDisplayElement.style.zIndex = '10000'; subtitleDisplayElement.style.textAlign = 'center'; subtitleDisplayElement.style.maxWidth = '80%'; subtitleDisplayElement.style.whiteSpace = 'pre-wrap'; subtitleDisplayElement.style.backgroundColor = 'rgba(0, 0, 0, 0.5)'; subtitleDisplayElement.style.padding = '10px'; subtitleDisplayElement.style.borderRadius = '5px'; const videoElement = document.querySelector('video.vjs-tech'); if (videoElement) { videoElement.parentElement.appendChild(subtitleDisplayElement); } } async function translat###btitles(text, targetLang) { return new Promise((resolve, reject) => { const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`; GM_xmlhttpRequest({ method: 'GET', url: url, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data && data[0] && data[0][0] && data[0][0][0]) { resolve(data[0][0][0]); } else { reject('Translation failed: No translated text'); } } catch (error) { reject(error); } }, onerror: (error) => { reject(error); }, }); }); } async function handl###btitles() { const videoElement = document.querySelector('video.vjs-tech'); if (!videoElement) return; const tracks = videoElement.textTracks; if (!tracks || tracks.length === 0) return; const track = tracks[0]; track.mode = 'hidden'; track.oncuechange = () => { const activeCue = track.activeCues[0]; if (activeCue && isSubtitlesEnabled) { const originalText = activeCue.text; if (CONFIG.translat###btitles && originalText) { translat###btitles(originalText, targetLanguage) .then((translatedText) => { if (subtitleDisplayElement) { subtitleDisplayElement.textContent = translatedText; } }) .catch(() => {}); } } }; } window.addEventListener('load', () => { createDraggableIcon(); }); })();