Adobe Learn Enhancer

Disable captions automatically (after the user hits play) and make the video control bar transparent on Adobe Learn videos.

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

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

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

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

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

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

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

Advertisement:

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

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

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

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

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

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

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

Advertisement:

// ==UserScript==
// @name         Adobe Learn Enhancer
// @namespace    karim.adobe.learn
// @version      3.0.0
// @description  Disable captions automatically (after the user hits play) and make the video control bar transparent on Adobe Learn videos.
// @author       Karim Elgazar
// @match        *://*.adobe.com/*
// @match        *://video.tv.adobe.com/*
// @match        *://*.tv.adobe.com/*
// @run-at       document-start
// @icon         https://www.google.com/s2/favicons?sz=64&domain_url=adobe.com
// @grant        none
// @license      Apache License, Version 2.0
// ==/UserScript==

(function () {
    'use strict';

    const TAG = '[Adobe Learn Enhancer]';
    const log = (...a) => console.debug(TAG, ...a);

    // ---------- 1. Transparent control bar ----------
    // The player markup (inside the video iframe):
    //   <div class="controls pointer-events-auto" data-v-aa047c22 style="--controls-scale: 1;">
    // .controls has background-color: var(--bg-color) which is what's covering the video.
    const css = `
        .controls,
        .controls.pointer-events-auto,
        div.controls[class*="pointer-events"] {
            background-color: transparent !important;
            background-image: linear-gradient(
                to top,
                rgba(0, 0, 0, 0.55) 0%,
                rgba(0, 0, 0, 0.25) 60%,
                rgba(0, 0, 0, 0) 100%
            ) !important;
        }
        .controls .v-button,
        .controls .controls__timestamp {
            filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.9));
            text-shadow: 0 1px 2px rgba(0, 0, 0, 0.9);
            opacity: 1 !important;
        }

        /* Hide the top-left chapter-marker bubble. */
        .chapter-marker {
            display: none !important;
        }
    `;

    function injectStyle() {
        if (document.getElementById('adobe-learn-enhancer-style')) return;
        const style = document.createElement('style');
        style.id = 'adobe-learn-enhancer-style';
        style.textContent = css;
        (document.head || document.documentElement).appendChild(style);
    }
    injectStyle();
    document.addEventListener('DOMContentLoaded', injectStyle, { once: true });

    // ---------- 2. Disable captions when they appear ----------
    // Goal: whenever the closed-captions button is reachable AND reports it's ON
    // (aria-pressed="true"), click it once to turn captions OFF. We don't know
    // exactly when the player will mount its controls — it might be on hydrate,
    // it might be only after the user presses play — so we use three triggers:
    //   (a) a MutationObserver on the whole document
    //   (b) a listener on every <video>'s "play" event
    //   (c) a slow polling interval as a final safety net
    const CC_SELECTOR =
        'button[aria-label="closed captions" i], button[aria-label*="caption" i]';
    const handled = new WeakSet();

    function disableCaptionsOnce(btn) {
        if (handled.has(btn)) return false;
        const pressed = btn.getAttribute('aria-pressed');
        if (pressed === 'true') {
            try {
                btn.click();
                handled.add(btn);
                log('Clicked CC button to turn captions OFF.');
                return true;
            } catch (e) {
                console.warn(TAG, 'CC click failed:', e);
            }
        } else if (pressed === 'false') {
            // Already off — remember it so we don't fight the user if they
            // turn captions back on manually.
            handled.add(btn);
        }
        return false;
    }

    function forceTextTracksOff() {
        document.querySelectorAll('video').forEach((v) => {
            if (!v.textTracks || !v.textTracks.length) return;
            for (const t of v.textTracks) {
                if (t.mode === 'showing') {
                    t.mode = 'disabled';
                    log('Forced textTrack OFF:', t.label || t.language || '(no label)');
                }
            }
        });
    }

    function scanAndDisable() {
        let acted = false;
        document.querySelectorAll(CC_SELECTOR).forEach((btn) => {
            if (disableCaptionsOnce(btn)) acted = true;
        });
        forceTextTracksOff();
        return acted;
    }

    // (a) Watch the DOM for the CC button being added or its aria-pressed flipping to "true".
    const mo = new MutationObserver((mutations) => {
        let shouldScan = false;
        for (const m of mutations) {
            if (m.type === 'attributes' && m.attributeName === 'aria-pressed') {
                shouldScan = true;
                break;
            }
            if (m.type === 'childList' && m.addedNodes.length) {
                shouldScan = true;
                break;
            }
        }
        if (shouldScan) scanAndDisable();
    });
    function startObserver() {
        mo.observe(document.documentElement, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['aria-pressed'],
        });
    }
    startObserver();

    // (b) Hook every <video> we ever see, so we can react the moment the user hits play.
    const wiredVideos = new WeakSet();
    function wireVideos() {
        document.querySelectorAll('video').forEach((v) => {
            if (wiredVideos.has(v)) return;
            wiredVideos.add(v);
            const onPlay = () => {
                log('Video play event — scanning for CC button.');
                // Run a few times because the controls bar may animate in.
                scanAndDisable();
                setTimeout(scanAndDisable, 150);
                setTimeout(scanAndDisable, 600);
                setTimeout(scanAndDisable, 1500);
            };
            v.addEventListener('play', onPlay);
            v.addEventListener('playing', onPlay);
            log('Wired play listener on a <video>.');
        });
    }
    // Re-run wiring whenever the DOM changes.
    new MutationObserver(wireVideos).observe(document.documentElement, {
        childList: true,
        subtree: true,
    });
    document.addEventListener('DOMContentLoaded', wireVideos, { once: true });

    // (c) Safety-net poll (cheap, every 1s for up to 60s).
    let tries = 0;
    const poll = setInterval(() => {
        tries++;
        wireVideos();
        scanAndDisable();
        if (tries >= 60) clearInterval(poll);
    }, 1000);

    console.info(TAG, 'v3.0.0 active in', location.host, location.pathname);
})();