Coursera Transcript to Right Side

Move the Coursera video transcript panel to the right side of the player and

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Coursera Transcript to Right Side
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  Move the Coursera video transcript panel to the right side of the player and
//               automatically scroll the transcript to follow the currently playing sentence.
// @author       memoverflow
// @match        https://www.coursera.org/*
// @grant        GM_addStyle
// @license      MIT
// @homepage     https://github.com/memoverflow/memoverflow-coursera-transcript-right
// @supportURL   https://github.com/memoverflow/memoverflow-coursera-transcript-right/issues
// ==/UserScript==

(function() {
    'use strict';

    // Inject CSS to reposition the transcript panel; avoids heavy DOM mutation.
    // Keeps the change lightweight and resilient to small DOM updates on the site.
    GM_addStyle(`
        /* Shrink the main video/player container to make room for the transcript */
        [data-testid="inner-container"] {
            width: calc(100% - 450px) !important;
            max-width: calc(100% - 500px) !important;
        }

        /* Transcript / Notes / Downloads area - keep fixed on the right side */
        [class*="TabPanel"],
        [class*="ItemPageContent"],
        .rc-Transcript,
        [class*="transcript"] {
            position: fixed !important;
            right: 10px !important;
            top: 70px !important;
            width: 480px !important;
            max-height: calc(100vh - 90px) !important;
            overflow-y: auto !important;
            background: #fff !important;
            border: 1px solid #e0e0e0 !important;
            border-radius: 8px !important;
            padding: 15px !important;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
            z-index: 1000 !important;
        }

        /* Hide duplicate transcript containers that may be injected by the site */
        .video-transcript-container {
            display: none !important;
        }

        /* Each transcript cue (data-cue) on its own line with dashed border */
        .rc-Phrase,
        [class*="rc-Phrase"],
        [data-cue] {
            display: block !important;
            margin-bottom: 8px !important;
            padding-bottom: 8px !important;
            border-bottom: 1px dashed #ccc !important;
            font-size: 1em !important;
            font-weight: 400 !important;
            transition: font-size 0.3s ease, font-weight 0.3s ease, transform 0.3s ease !important;
        }
    `);

    console.log('Coursera Transcript to Right Side - CSS loaded');

    // Return true if the primary <video> element is currently playing
    function isVideoPlaying() {
        const video = document.querySelector('video');
        if (video) {
            return !video.paused && !video.ended;
        }
        return false;
    }

    // Auto-scroll the transcript panel to the currently highlighted sentence
    function scrollToActiveTranscript() {
        // Only auto-scroll while the video is playing
        if (!isVideoPlaying()) {
            return;
        }

        // Find the currently highlighted sentence
        const activePhrase = document.querySelector(
            '[class*="transcript"] [class*="active"],' +
            '[class*="transcript"] [class*="highlight"],' +
            '[class*="transcript"] [class*="current"],' +
            '[class*="Transcript"] [class*="active"],' +
            '[class*="Transcript"] [class*="highlight"],' +
            '[class*="Transcript"] [class*="current"],' +
            '.rc-Transcript [class*="active"],' +
            '.rc-Transcript [class*="highlight"],' +
            '[class*="phrase"][class*="active"],' +
            '[class*="cue"][class*="active"],' +
            // Find highlighted element by background color
            '[style*="background"]'
        );

        if (activePhrase) {
            // Smoothly center the highlighted sentence in the transcript container
            activePhrase.scrollIntoView({
                behavior: 'smooth',
                block: 'center'
            });
        }
    }

    // Observe the transcript container for attribute and subtree changes
    // that indicate the active/highlighted sentence changed, then scroll it.
    function setupTranscriptObserver() {
        const transcriptContainer = document.querySelector(
            '[class*="transcript"],' +
            '[class*="Transcript"],' +
            '.rc-Transcript'
        );

        if (!transcriptContainer) {
            // If the transcript element isn't available yet, try again shortly.
            setTimeout(setupTranscriptObserver, 1000);
            return;
        }

        // Watch for attribute and subtree modifications indicating highlight changes
        const observer = new MutationObserver(() => {
            scrollToActiveTranscript();
        });

        observer.observe(transcriptContainer, {
            attributes: true,
            subtree: true,
            attributeFilter: ['class', 'style']
        });

        console.log('Transcript auto-scroll observer setup complete');
    }

    // Apply active styles via JavaScript as a fallback
    function applyActiveStyles() {
        // Reset all phrases to normal style
        document.querySelectorAll('.rc-Phrase, [data-cue]').forEach(el => {
            el.style.fontSize = '';
            el.style.fontWeight = '';
            el.style.color = '';
            el.style.borderBottomColor = '';
        });

        // Find and style the active phrase
        const activeEl = document.querySelector('.rc-Phrase.active, [data-cue].active');
        if (activeEl) {
            activeEl.style.fontSize = '1.3em';
            activeEl.style.fontWeight = '700';
            activeEl.style.color = '#1a73e8';
            activeEl.style.borderBottomColor = '#1a73e8';
        }
    }

    // Also check periodically to ensure nothing is missed
    setInterval(() => {
        scrollToActiveTranscript();
        applyActiveStyles();
    }, 500);

    // Start observer after page loads
    setTimeout(setupTranscriptObserver, 2000);
})();