Greasy Fork 支持简体中文。

Youtube subtitles under video frame

Have you ever been annoyed by youtube subtitles covering some important part of the video? No more! The userscript moves subtitles under video frame (but you can still drag-move them horizontally). It works for default and theater modes.

目前為 2023-07-11 提交的版本,檢視 最新版本

/* 
    Youtube subtitles under video frame: Move youtube subtitles under video frame.
	Copyright (C) 2023  T1mL3arn

    This program is free software: you can redistribute it and/or modify 
    it under the terms of the GNU General Public License as published by 
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version. 
	
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

// ==UserScript==
// @name        Youtube subtitles under video frame 
// @name:RU     Субтитры Youtube под видео
// @description Have you ever been annoyed by youtube subtitles covering some important part of the video? No more! The userscript moves subtitles under video frame (but you can still drag-move them horizontally). It works for default and theater modes. 
// @description:RU  Вам когда-нибудь мешали субтитры Youtube, закрывыющие какую-то важную область видео? Пора это прекратить! Этот скрипт сдвигает субтитры под видео (вы все еще можете перетаскивать их по горизонтали). Работает в режимах "обычный" и "широкий экран".
// @namespace   https://github.com/t1ml3arn-userscript-js
// @version     1.4.3
// @match       https://www.youtube.com/*
// @match       https://youtube.com/*
// @grant       none
// @noframes
// @author      T1mL3arn
// @icon        data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGRlZnM+PHN5bWJvbCBpZD0iYSIgdmlld0JveD0iMCAwIDM0NCA1OSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PHJlY3QgZmlsbD0icmdiYSgyNTUsIDI1NSwgMjU1LCAwKSIgaGVpZ2h0PSIxMDAlIiB3aWR0aD0iMTAwJSIvPjxwYXRoIGQ9Ik0tNTAtNTBINTBWNTBILTUwVi01MHoiIGZpbGw9IiNGRkYiIGZpbGwtb3BhY2l0eT0iMCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNTAgNTApIi8+PHBhdGggZD0iTS01MC01MEg1MFY1MEgtNTBWLTUweiIgZmlsbD0iI0ZGRiIgZmlsbC1vcGFjaXR5PSIwIiB0cmFuc2Zvcm09Im1hdHJpeCguNDUgMCAwIC43NiAxNTYuNDEgMzY5Ljc1KSIvPjxwYXRoIGQ9Ik0tNTAtNTBWNTBINTBWLTUwSC01MHoiIGZpbGw9IiNGRkYiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLXdpZHRoPSIwIiB0cmFuc2Zvcm09Im1hdHJpeCgyLjQxIDAgMCAuMiAxMjAuNSAxMCkiLz48cGF0aCBkPSJNLTUwLTUwVjUwSDUwVi01MEgtNTB6IiBmaWxsPSIjRkZGIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMCIgdHJhbnNmb3JtPSJtYXRyaXgoLjcgMCAwIC4yIDMxMC4xOCAxMCkiLz48cGF0aCBkPSJNLTUwLTUwVjUwSDUwVi01MEgtNTB6IiBmaWxsPSIjRkZGIiBzdHJva2U9IiMwMDAiIHN0cm9rZS13aWR0aD0iMCIgdHJhbnNmb3JtPSJtYXRyaXgoLTIuNDEgMCAwIC0uMiAyMjQuNjggNTApIi8+PHBhdGggZD0iTS01MC01MFY1MEg1MFYtNTBILTUweiIgZmlsbD0iI0ZGRiIgc3Ryb2tlPSIjMDAwIiBzdHJva2Utd2lkdGg9IjAiIHRyYW5zZm9ybT0ibWF0cml4KC0uNyAwIDAgLS4yIDM1IDUwKSIvPjwvc3ltYm9sPjwvZGVmcz48ZyBjbGFzcz0ibGF5ZXIiPjxwYXRoIGQ9Ik00OTguMjk0IDU3LjI5OWMtNS44MjgtMjEuNzc2LTIyLjk0Mi0zOC44ODgtNDQuNzE4LTQ0LjcxN0M0MTQuMTUgMi4wMDYgMjU1Ljk3MiAyLjAwNiAyNTUuOTcyIDIuMDA2cy0xNTguMTc0IDAtMTk3LjYwMyAxMC41NzZDMzYuNTkzIDE4LjQxIDE5LjQ4IDM1LjUyMyAxMy42NTIgNTcuMjk5IDMuMDc2IDk2LjcyOCAzLjA3NiAxNzkuMDQyIDMuMDc2IDE3OS4wNDJzMCA4Mi4zMTUgMTAuNTc2IDEyMS43NDRjNS44MjkgMjEuNzc2IDIyLjk0MSAzOC44ODggNDQuNzE3IDQ0LjcxNiAzOS40MjkgMTAuNTc2IDE5Ny42MDMgMTAuNTc2IDE5Ny42MDMgMTAuNTc2czE1OC4xNzcgMCAxOTcuNjA0LTEwLjU3NmMyMS43NzYtNS44MjggMzguODktMjIuOTQgNDQuNzE4LTQ0LjcxNiAxMC41NzYtMzkuNDMgMTAuNTc2LTEyMS43NDQgMTAuNTc2LTEyMS43NDRzLS4wNDItODIuMzE0LTEwLjU3Ni0xMjEuNzQzeiIgZmlsbD0icmVkIi8+PHBhdGggZD0iTTAgLjVoNTEydjUxMkgwVi41eiIgZmlsbD0idHJhbnNwYXJlbnQiLz48cGF0aCBkPSJNNDMuNzg4IDM3MWg0MjQuNDI3djEzMS4zMkg0My43ODhWMzcxeiIvPjx1c2UgdHJhbnNmb3JtPSJtYXRyaXgoLjczNjA4IDAgMCAuOTYyMTcgLTc1Ljk5IC00ODUuMDY1KSIgeD0iMTk0LjE0NCIgeGxpbms6aHJlZj0iI2EiIHk9IjcwMiIvPjxwYXRoIGQ9Im0yMDUuMzQ1IDI1NS4wODYgMTMxLjQwNC03NS44Ni0xMzEuNDA0LTc1Ljg2djE1MS43MnoiIGZpbGw9IiNGRkYiLz48L2c+PC9zdmc+
// @homepageURL https://github.com/t1ml3arn-userscript-js/Youtube-subtitles-under-video-frame
// @supportURL  https://github.com/t1ml3arn-userscript-js/Youtube-subtitles-under-video-frame/issues
// @license     GPL-3.0-or-later
// ==/UserScript==

const SUBS_BUTTON_SELECTOR = '.ytp-subtitles-button'
const USERJS_ELT_CLASS = 'yfms-userjs'
const USERJS_STYLE_ID = 'youtube-subs-under-video-css'
const PLAYER_ELT_SELECTOR = 'ytd-watch-flexy'
const SUBS_GAP = 64;
const SUBS_GAP_THEATER = 100;

const USERJS_STYLE_CONTENT = `
.${USERJS_ELT_CLASS} {
    --subs-gap: ${SUBS_GAP}px;
    --subs-gap-theater: ${SUBS_GAP_THEATER}px;
}

.${USERJS_ELT_CLASS}:not([fullscreen]) .caption-window.ytp-caption-window-bottom {
    margin-bottom: 0 !important;
    margin-top: 0 !important;
    position: absolute !important;
    bottom: 0 !important;
    top: calc(100% + 16px) !important;
    z-index: 9999 !important;
}

.${USERJS_ELT_CLASS} .html5-video-player {
    /* to make subs visible when they are outside player frame */
    overflow: visible;
    /* to make player to be on top 
    (combined with captions z-index rule, 
    it places captions over any element on the page) */
    z-index: 999;
}

.${USERJS_ELT_CLASS} #movie_player.ended-mode .html5-video-container > video,
.${USERJS_ELT_CLASS} #movie_player.unstarted-mode .html5-video-container > video,
 {
    visibility: hidden;
}

.${USERJS_ELT_CLASS} #below {
    margin-top: var(--subs-gap);
    transition: margin-top 0.25s;
}

.${USERJS_ELT_CLASS}[theater]:not([fullscreen]) #below {
    margin-top: var(--subs-gap-theater);
}

/* styling for "related videos" section */
.${USERJS_ELT_CLASS}[theater]:not([fullscreen]) #secondary.ytd-watch-flexy {
    margin-top: var(--subs-gap-theater);
    transition: margin-top 0.25s;
}
`

let canToggleSubsWithKeyboard = true;
const ccSizes = {
    0: 64,
    1: 80,
}

function addStyles(css, id) {
    const style = document.head.appendChild(document.createElement('style'))
    style.textContent = css;
    style.id = id
}

function displaceSubtitles(subsButton) {

    const pressed = subsButton.getAttribute('aria-pressed')

    // this elt gets special attribute by youtube when view mode changes,
    // so it also gets my marker class to apply my CSS
    const playerElt = document.querySelector(PLAYER_ELT_SELECTOR)
    if (pressed === 'true')
        playerElt.classList.add(USERJS_ELT_CLASS)
    else if (pressed === 'false')
        playerElt.classList.remove(USERJS_ELT_CLASS)
}

function onSubsClick(e) {
    displaceSubtitles(e.currentTarget)
}

function getCaptionsButton() {
    return getVisibleElt(SUBS_BUTTON_SELECTOR)
}

function getVisibleElt(selector) {
    return Array.from(document.querySelectorAll(selector)).find(e => e.offsetParent !==null)
}

function isItVideoPage() {
    return window.location.search.includes('v=')
}

function toggleSubtitlesKeyDown(e) {
    const subsButton = getCaptionsButton()
    
    if (e.code === 'KeyC' || e.keyCode === 67)
    if (isItVideoPage() && subsButton && canToggleSubsWithKeyboard) {
        displaceSubtitles(subsButton)
    }
}

function onFocusIn(e) {

    // disable captions toggling 
    // if user focused any input element (like search bar or comment textarea)
    if (e.target.tagName === 'INPUT' || 
    e.target.matches('div#contenteditable-root.style-scope.yt-formatted-string')) {
        canToggleSubsWithKeyboard = false
    } else {
        canToggleSubsWithKeyboard = true
    }
}

function onFocusOut(e) {

    // restoring captions toggling if user focused out
    // input elements
    if (e.target.tagName === 'INPUT' || 
    e.target.matches('div#contenteditable-root.style-scope.yt-formatted-string')) {
        canToggleSubsWithKeyboard = true
    }
}

function enchanceSubsButton() {
    if (isItVideoPage()) {

        const subsButton = getCaptionsButton()

        if (!subsButton) {
            console.debug(`Video ${window.location.href} has no subtitles button`);
            return
        }

        displaceSubtitles(subsButton)
        updateGapSize()

        document.addEventListener('keydown', toggleSubtitlesKeyDown )
        subsButton.addEventListener('click', onSubsClick)
    }
}

function localStorageHook() {
    
    let original = Storage.prototype.setItem;
    Storage.prototype.setItem = function() {
        const event = new CustomEvent('storageSetItem', {
            detail: { 
                key: arguments[0], 
                value: arguments[1]
            },
        });
        original.apply(this, arguments);
        window.dispatchEvent(event);
    }
}

function getGapSize(f, initial) {
    // the formula is found in youtube js code
    return initial * (1 + 0.25 * Math.max(f || 0, 0));
}

function updateGapSize() {
    // NOTE CC visible size remains FIXED even if a user changes
    // zoom level in his browser!

    const raw = localStorage.getItem('yt-player-caption-display-settings')
    let ccDisplaySettings;
    try {
        ccDisplaySettings = JSON.parse(JSON.parse(raw).data)
    } catch(e) {
        ccDisplaySettings = {}
    }
    const fontSizeIncrement = ccDisplaySettings.fontSizeIncrement || 0
    let newGap = getGapSize(fontSizeIncrement, SUBS_GAP);
    document.querySelector(PLAYER_ELT_SELECTOR).style.setProperty('--subs-gap', `${newGap}px`)
    newGap = getGapSize(fontSizeIncrement, SUBS_GAP_THEATER)
    document.querySelector(PLAYER_ELT_SELECTOR).style.setProperty('--subs-gap-theater', `${newGap}px`)
}

function init() {
    addStyles(USERJS_STYLE_CONTENT, USERJS_STYLE_ID)

    document.addEventListener('focusin', onFocusIn)
    document.addEventListener('focusout', onFocusOut)

    // Hint about youtube-specific events was found there
    // https://stackoverflow.com/questions/34077641/how-to-detect-page-navigation-on-youtube-and-modify-its-appearance-seamlessly/34100952#34100952
    document.addEventListener('yt-navigate-finish', enchanceSubsButton)
    document.addEventListener('yt-page-data-updated', enchanceSubsButton)

    localStorageHook();

    window.addEventListener('storageSetItem', e => {
        const { key } = e.detail;

        if (key != 'yt-player-caption-display-settings') 
            return
        
        updateGapSize()
    })
}

init();