您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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.
/* 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.5.1 // @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 KEY__PLAYER_CAPTION_DISPLAY_SETTINGS = 'yt-player-caption-display-settings' const KEY__PLAYER_STICKY_CAPTION = 'yt-player-sticky-caption' 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; } ytd-player:not([fullscreen]):not([theater]) { /* in default mode "ytd-player" has "overflow: hidden" thus hiding the subs, this rule makes it visible again */ overflow: visible !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, .${USERJS_ELT_CLASS} #movie_player.unstarted-mode .html5-video-container { /* By default this container has no height, setting height explicitly prevents hiding of video. This actually not needed since "overflow: hidden" happens before start and after end, but I want to be sure. */ height: 100%; /* video frame move above when video ends, without hiding a user can see part of the video above player */ overflow: 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(below = true) { // 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 (below) playerElt.classList.add(USERJS_ELT_CLASS) else playerElt.classList.remove(USERJS_ELT_CLASS) } function onSubsClick() { displaceSubtitles(areSubsEnabled()) } 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 areSubsAvailable() { const subsButton = getCaptionsButton() if (!subsButton) { console.debug(`Video ${window.location.href} has no subtitles button`); return false } // Video may have no subs at all, to catch that case // I can only check button's opacity const btnIcon = subsButton.querySelector('svg') if (parseFloat(btnIcon.getAttribute("fill-opacity") || 1) != 1) return false return true } function toggleSubtitlesKeyDown(e) { if (e.code === 'KeyC' || e.keyCode === 67) if (isItVideoPage() && areSubsAvailable() && canToggleSubsWithKeyboard) { displaceSubtitles(areSubsEnabled()) } } 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()) { if (!areSubsAvailable()) return const subsButton = getCaptionsButton() let subsEnabled = areSubsEnabled() // sometimes I cannot rely on local storage setting // to get subtitles state but I still can get it // from the caption button ARIA attribute let subsButtonPressed = subsButton.getAttribute('aria-pressed') === 'true' displaceSubtitles(subsEnabled || subsButtonPressed) // forcing YT to enable subs if (subsEnabled && !subsButtonPressed) { subsButton.click() } 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) { // this 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(KEY__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`) } /** Checks if local storage has a setting for * enabled/disabled subs. * * **NOTE**: This setting might not be in local storage even * if a registered YT user enabled subtitles globally * with YT settings. * @returns {Bool} */ function areSubsEnabled() { const raw = localStorage.getItem(KEY__PLAYER_STICKY_CAPTION) try { return JSON.parse(JSON.parse(raw).data) } catch(e) { return false } } 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; // console.log(`YT local storage "${key}" was updated`) switch (key) { case KEY__PLAYER_CAPTION_DISPLAY_SETTINGS: updateGapSize() break; case KEY__PLAYER_STICKY_CAPTION: // if sticky subs are enabled - displace subs displaceSubtitles(areSubsEnabled()) break; default: break; } }) } init();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址