PKGA YouTube Theater Mode

A more fullscreen experience for theater mode

  1. // ==UserScript==
  2. // @name PKGA YouTube Theater Mode
  3. // @description A more fullscreen experience for theater mode
  4. // @license GNU GPLv3
  5. // @namespace /lig/
  6. // @version 13
  7. // @grant GM.info
  8. // @include https://*.youtube.com/*
  9. // ==/UserScript==
  10. const appendStyle = (css, id = '', doc = document) => {
  11. const style = document.createElement('style');
  12. style.id = id;
  13. style.innerHTML = css;
  14. doc.head.appendChild(style);
  15. return style;
  16. }
  17.  
  18. // main style
  19. const mainCSS = `
  20. #ytc-filter .vc-toolbar button[title="Show ytcFilter"] {
  21. right: 0 !important;
  22. position: absolute;
  23. }
  24.  
  25. button.html5-video-info-panel-close {
  26. left: 5px;
  27. right: unset !important;
  28. }
  29.  
  30. .seventv-yt-theater-mode-button-container {
  31. display: none;
  32. }
  33.  
  34. .seventv-overlay > div > div {
  35. transform: none !important;
  36. width: 100%;
  37. height: 100%;
  38. }
  39.  
  40. .seventv-emote-menu {
  41. box-sizing: border-box;
  42. left: unset !important;
  43. max-width: 100% !important;
  44. max-height: calc(100% - 105px);
  45. }
  46.  
  47. .seventv-emote-menu .seventv-emote-menu-tab {
  48. max-height: 30px !important;
  49. min-height: 20px;
  50. }
  51.  
  52. .seventv-emote-menu-header > button:after {
  53. color: white !important;
  54. font-weight: bold;
  55. margin: auto;
  56. margin-left: 5px;
  57. }
  58.  
  59. .seventv-emote-menu-header > button:nth-child(1):after {
  60. content: "7TV";
  61. }
  62.  
  63. .seventv-emote-menu-header > button:nth-child(2):after {
  64. content: "BTTV";
  65. }
  66.  
  67. .seventv-emote-menu-header > button:nth-child(3):after {
  68. content: "FFZ";
  69. }
  70.  
  71. .seventv-emote-menu-header > button {
  72. border-color: #aaa;
  73. border-width: 1px;
  74. border-radius: .4rem;
  75. }
  76.  
  77. .seventv-emote-menu-header > button.selected {
  78. border-color: #fff;
  79. }
  80.  
  81. .seventv-emote-menu .seventv-emote-menu-scrollable {
  82. padding-top: 0;
  83. }
  84.  
  85. .seventv-emote-menu .seventv-emote-menu-tab img {
  86. width: auto;
  87. height: 25px;
  88. margin-left: auto;
  89. }
  90.  
  91. .seventv-emote-menu-scrollable {
  92. max-height: calc();
  93. }
  94.  
  95. .iaextractor-webx-iframe {
  96. z-index: 10000;
  97. }
  98.  
  99. * {
  100. scrollbar-color: #727273 #171719;
  101. }
  102.  
  103. ::-webkit-scrollbar-button {
  104. background: #727273 !important;
  105. }
  106.  
  107. ::-webkit-scrollbar-thumb {
  108. background: #727273 !important;
  109. }
  110.  
  111. ::-webkit-scrollbar-track {
  112. background: #171719 !important;
  113. }
  114.  
  115. app-drawer[opened] {
  116. z-index: 10000
  117. }
  118.  
  119. #clarify-box {
  120. display:none !important;
  121. }
  122.  
  123. /* Chat container */
  124. body.theater ytd-live-chat-frame#chat {
  125. /*min-height: 0 !important;
  126. height: 40px !important;
  127. min-width: 0 !important;
  128. width: 50px;
  129. overflow: hidden;
  130. border: hidden;*/
  131. margin: 0 !important;
  132. position: absolute;
  133. top: 0 !important;
  134. right: 0;
  135. width: calc(12.79vw) !important;
  136. min-width: 285px;
  137. max-width: 440px;
  138. border-width: 0px !important;
  139. pointer-events: none;
  140. overflow: visible !important;
  141. }
  142.  
  143. /* Chat panel */
  144. body.theater ytd-live-chat-frame#chat iframe.ytd-live-chat-frame {
  145. position: absolute;
  146. top: 0px;
  147. right: 0;
  148. width: calc(12.79vw) !important;
  149. min-width: 285px;
  150. max-width: 440px;
  151. height: 100vh;
  152. z-index: 0;
  153. pointer-events: auto;
  154. }
  155.  
  156. ytd-watch-flexy[fullscreen] #full-bleed-container.live-chat ~ #columns #chat iframe {
  157. height: calc(100vh - 75px);
  158. }
  159.  
  160. body.theater.live-chat-top ytd-live-chat-frame#chat iframe.ytd-live-chat-frame {
  161. top: var(--video-height);
  162. max-width: 100vw;
  163. width: 100vw !important;
  164. height: var(--chat-height);
  165. }
  166.  
  167. /* Toggle Button */
  168. #show-hide-button tp-yt-paper-button#button.ytd-toggle-button-renderer {
  169. z-index: 1001;
  170. position: absolute;
  171. top: 0px;
  172. right: 0;
  173. /*width: 30px !important;*/
  174. height: 30px;
  175. padding: 3px !important;
  176. pointer-events: auto;
  177. /*transform: translate(300px, 0);*/
  178. }
  179.  
  180. #show-hide-button yt-formatted-string#text.ytd-toggle-button-renderer {
  181. width: 40px;
  182. filter: drop-shadow(0 0 5px #000);
  183. }
  184.  
  185. #show-hide-button paper-ripple {
  186. width: 40px;
  187. }
  188.  
  189. .live-chat-top #show-hide-button tp-yt-paper-button#button.ytd-toggle-button-renderer[aria-pressed="false"] {
  190. /*top: calc(var(--video-height));*/
  191. }
  192.  
  193. @media only screen and (max-width: 750px) {}
  194.  
  195. /* super chat ticker */
  196. .live-chat-top yt-live-chat-renderer #ticker.yt-live-chat-renderer {
  197. left: 127px;
  198. right: 107px;
  199. transform: translateY(-40px);
  200. position: absolute;
  201. z-index: 10000;
  202. }
  203.  
  204. .live-chat-top yt-live-chat-renderer #ticker.yt-live-chat-renderer #items {
  205. padding-bottom: 0;
  206. }
  207.  
  208. .live-chat-top yt-live-chat-renderer #contents {
  209. overflow: visible !important;
  210. }
  211.  
  212. /* Resize the player in theater mode only */
  213. ytd-watch-flexy[theater]:not([fullscreen]) #full-bleed-container.ytd-watch-flexy {
  214. max-height: 100vh;
  215. height: 100vh;
  216. }
  217.  
  218. /* Live Chat Variant */
  219. ytd-watch-flexy[theater]:not([fullscreen]) #full-bleed-container.ytd-watch-flexy.live-chat,
  220. ytd-watch-flexy[fullscreen] #full-bleed-container.ytd-watch-flexy.live-chat {
  221. left: 0;
  222. right: min(440px, max(285px, calc(12.79vw)));
  223. width: calc(100vw - min(440px, max(285px, calc(12.79vw)))) !important;
  224. }
  225.  
  226. .live-chat-top ytd-watch-flexy[theater]:not([fullscreen]) #full-bleed-container.ytd-watch-flexy.live-chat {
  227. width: 100vw !important;
  228. height: var(--video-height);
  229. margin-bottom: var(--chat-height);
  230. right: 0;
  231. min-height: 0 !important;
  232. }
  233.  
  234. ytd-watch-flexy[fullscreen] #full-bleed-container.ytd-watch-flexy.live-chat .html5-video-container {
  235. width: 100%;
  236. height: 100%;
  237. }
  238.  
  239. ytd-watch-flexy[fullscreen] #full-bleed-container.ytd-watch-flexy.live-chat .html5-video-container video {
  240. width: 100% !important;
  241. }
  242.  
  243. body.theater #show-hide-button {
  244. pointer-events: all;
  245. width: 102px;
  246. left: 114px;
  247. position: absolute;
  248. }
  249.  
  250. #show-hide-button button {
  251. height: 30px;
  252. }
  253.  
  254. body.theater.live-chat-top #show-hide-button {
  255. top: var(--video-height);
  256. }
  257.  
  258. .yt-spec-button-shape-next--button-text-content {
  259. text-overflow: clip;
  260. width: 64px;
  261. }
  262.  
  263. #ytc-filter,
  264. #ytc-filter > div,
  265. #ytc-filter .vc-toolbar {
  266. pointer-events: none;
  267. }
  268.  
  269. div.vc-toolbar > * {
  270. pointer-events: all;
  271. }
  272.  
  273. body.no-ytc-filter yt-live-chat-header-renderer {
  274. margin-top: 30px;
  275. }
  276.  
  277. body.no-ytc-filter #show-hide-button {
  278. left: unset;
  279. right: 0;
  280. }
  281.  
  282. body.theater #show-hide-button.chat-hidden {
  283. top: 0;
  284. right: 0;
  285. left: unset;
  286. width: unset;
  287. }
  288.  
  289. body.theater #show-hide-button.chat-hidden .yt-spec-button-shape-next--button-text-content {
  290. width: unset;
  291. }
  292.  
  293. `;
  294. appendStyle(mainCSS, 'pkga-css');
  295.  
  296. const watchCSS = `
  297. html::-webkit-scrollbar,
  298. body::-webkit-scrollbar {
  299. display: none;
  300. }
  301.  
  302. body, html {
  303. scrollbar-width: none;
  304. }
  305.  
  306. body.theater #masthead-container {
  307. position: fixed;
  308. opacity: 0;
  309. transition: opacity 0.25s, transform 0.3s ease !important;
  310. pointer-events: none;
  311. }
  312.  
  313. body.theater #masthead-container:hover,
  314. body.theater #masthead-container:focus,
  315. body.theater #masthead-container.show-header {
  316. opacity: 1;
  317. }
  318.  
  319. body.theater ytd-searchbox.ytd-masthead {
  320. margin-left: 9px;
  321. }
  322.  
  323. #search-icon-legacy.ytd-searchbox {
  324. width: 35px;
  325. }
  326.  
  327. body.theater #masthead {
  328. border-bottom-left-radius: 15px;
  329. border-bottom-right-radius: 15px;
  330. box-shadow: 0 0 20px #000a;
  331. margin: 0 auto;
  332. pointer-events: auto;
  333. }
  334.  
  335. body.theater #masthead,
  336. body.theater #masthead #background {
  337. width: calc(100vw - 700px);
  338. max-width: min(1750px, calc(100vw - 2*min(440px, max(285px, calc(12.79vw)))));;
  339. min-width: 750px;
  340. }
  341.  
  342. body.theater.live-chat-top #masthead,
  343. body.theater.live-chat-top #masthead #background {
  344. width: calc(100vw - 200px);
  345. max-width: unset;
  346. min-width: 450px;
  347. }
  348.  
  349. body.theater #masthead-bg {
  350. position: fixed;
  351. z-index: -1;
  352. background: #0006;
  353. pointer-events: none;
  354. width: 100vw;
  355. height: 100vh;
  356. top: 0;
  357. left: 0;
  358. transition: opacity 0.25s;
  359. opacity: 0;
  360. }
  361.  
  362. body.theater .show-header #masthead-bg {
  363. opacity: 0;
  364. }
  365.  
  366. body.theater #page-manager {
  367. margin-top: 0 !important;
  368. }
  369.  
  370. ytd-watch-flexy[theater] #secondary {
  371. position: unset !important;
  372. }
  373.  
  374. ytd-watch-flexy[flexy][js-panel-height_] #chat.ytd-watch-flexy:not([collapsed]).ytd-watch-flexy, ytd-watch-flexy[flexy][js-panel-height_] #chat-container.ytd-watch-flexy:not([chat-collapsed]).ytd-watch-flexy {
  375. height: var(--chat-height) !important;
  376. border-radius: 0 !important;
  377. }
  378.  
  379. body.live-chat-top ytd-watch-flexy[flexy][js-panel-height_] #chat.ytd-watch-flexy:not([collapsed]).ytd-watch-flexy, ytd-watch-flexy[flexy][js-panel-height_] #chat-container.ytd-watch-flexy:not([chat-collapsed]).ytd-watch-flexy {
  380. height: 0 !important;
  381. }
  382.  
  383. ytd-live-chat-frame[rounded-container] iframe.ytd-live-chat-frame {
  384. border-radius: 0 !important;
  385. }
  386.  
  387. body.theater.live-chat-top #chat {
  388. width: 100vw !important;
  389. left: 0 !important;
  390. right: 0 !important;
  391. max-width: 100vw !important;
  392. }
  393.  
  394. @media screen and (max-width: 1015px) {
  395. .live-chat-top #chat {
  396. top: calc(-14px - 100vh) !important;
  397. left: 0 !important;
  398. right: 0 !important;
  399. z-index: 100000;
  400. }
  401.  
  402. .live-chat-top #chatframe {
  403. left:-24px !important;
  404. right: 0 !important;
  405. }
  406. }
  407.  
  408. #ytd-player .html5-video-container {
  409. height: 100%;
  410. }
  411.  
  412. #ytd-player video {
  413. object-fit: contain;
  414. object-position: center;
  415. width: 100% !important;
  416. height: 100% !important;
  417. left: unset !important;
  418. right: unset !important;
  419. top: unset !important;
  420. bottom: unset !important;
  421. transform: none !important;
  422. }
  423.  
  424. body.theater #panels-full-bleed-container {
  425. width: min(440px, max(285px, calc(12.79vw))) !important;
  426. }
  427. `;
  428.  
  429. const waitingRoomCSS = `
  430. #ytp-offline-slate {
  431. max-height: calc(100vh - 56px);
  432. }
  433. `;
  434.  
  435. let delay = ms => new Promise(res => setTimeout(res, ms));
  436.  
  437. let checkForHTML = async (selector, element = document) => {
  438. let selected;
  439. while(true) {
  440. if(selected = element.querySelector(selector))
  441. return selected;
  442. await delay(100);
  443. }
  444. }
  445.  
  446. let getAnimationFrame = () => new Promise(res => requestAnimationFrame(res));
  447.  
  448. function getCookies() {
  449. let cookies = {};
  450. for(let [k,v] of document.cookie.split('; ').map(e => e.split('=')))
  451. cookies[k] = v;
  452. return cookies;
  453. }
  454.  
  455. if(parseInt(getCookies().wide || 0) == 1) {
  456. document.body.classList.add('theater');
  457. window.dispatchEvent(new Event('resize'));
  458. }
  459.  
  460. document.addEventListener('click', async e => {
  461. let target;
  462. if(target = e.target.closest('button.ytp-size-button')) {
  463. document.body.classList.toggle('theater');
  464. } else if(target = e.target.closest('#show-hide-button')) {
  465. await getAnimationFrame();
  466. let player = document.querySelector('#full-bleed-container.ytd-watch-flexy');
  467. if(target.innerText.toUpperCase().startsWith('SHOW CHAT')) {
  468. player.classList.remove('live-chat');
  469. target.classList.add('chat-hidden');
  470. } else {
  471. player.classList.add('live-chat');
  472. target.classList.remove('chat-hidden');
  473. }
  474. window.dispatchEvent(new Event('resize'));
  475. }
  476. });
  477.  
  478. let root = document.body.parentElement,
  479. header = document.querySelector('#masthead-container'),
  480. lastScroll = 0;
  481. document.addEventListener('scroll', e => {
  482. /*let currentScroll = Date.now();
  483. if(currentScroll - lastScroll < 100) return;
  484. lastScroll = currentScroll;*/
  485. if(!header) header = document.querySelector('#masthead-container');
  486. if(root.scrollTop != 0)
  487. header.classList.add('show-header');
  488. else header.classList.remove('show-header');
  489. });
  490.  
  491. function checkIfLiveChat() {
  492. return document.querySelector('#chat')
  493. && document.querySelector('#show-hide-button').innerText.toUpperCase().startsWith('HIDE CHAT');
  494. }
  495.  
  496. (async() => {
  497. let watchStyle;
  498. while(true) {
  499. (() => {
  500. let player = document.querySelector('ytd-watch-flexy:not([hidden]) #full-bleed-container'),
  501. video = player ? player.querySelector('video'):null,
  502. offline = document.querySelector('.ytp-offline-slate');
  503. if(player && (video && video.src || offline)) {
  504. //console.log('[PKGA Theater Mode] video', video);
  505. if(!watchStyle) {
  506. appendStyle(watchCSS, 'pkga-watch-css');
  507. getAnimationFrame().then(() => document.documentElement.scrollTop = 0);
  508. watchStyle = document.querySelector('#pkga-watch-css');
  509. }
  510. let isLiveChat = checkIfLiveChat();
  511. if(isLiveChat) {
  512. player.classList.add('live-chat');
  513. document.querySelector('#show-hide-button').classList.remove('chat-hidden');
  514. let chatDocument = document.querySelector('#chatframe').contentDocument;
  515. if(!chatDocument || !chatDocument.body || !chatDocument.head)
  516. return;
  517. if(!chatDocument.querySelector('#ytc-filter')) {
  518. document.body.classList.add('no-ytc-filter');
  519. chatDocument.body.classList.add('no-ytc-filter');
  520. } else {
  521. document.body.classList.remove('no-ytc-filter');
  522. chatDocument.body.classList.remove('no-ytc-filter');
  523. }
  524. if(!chatDocument.querySelector('#pkga-css'))
  525. appendStyle(mainCSS, 'pkga-css', chatDocument);
  526. //await getAnimationFrame();
  527. //window.dispatchEvent(new Event('resize'));
  528. } else if(player.classList.contains('live-chat')) {
  529. player.classList.remove('live-chat');
  530. //await getAnimationFrame();
  531. //window.dispatchEvent(new Event('resize'));
  532. }
  533. /*document.querySelector('#movie_player').classList.add('ytp-big-mode');*/
  534. } else if(watchStyle) {
  535. watchStyle.remove();
  536. watchStyle = null;
  537. }
  538. })();
  539. await delay(100);
  540. }
  541. })();
  542.  
  543. let oldVideo;
  544. (async() => {
  545. while(true) {
  546. let video = document.querySelector('ytd-watch-flexy:not([hidden]) #full-bleed-container video');
  547. if(!video) {
  548. if(video = document.querySelector('.ytp-offline-slate-background')) {
  549. video.src = video.getAttribute('style');
  550. video.offline = true;
  551. }
  552. }
  553. if(video && (!oldVideo || oldVideo.src != video.src)) {
  554. if(video.offline) {
  555. //console.log('[PKGA Theater Mode] Offline Stream Found');
  556. setTopChat();
  557. } else if(!video.videoWidth) {
  558. await new Promise(res => {
  559. video.addEventListener('loadedmetadata', res);
  560. });
  561. setTopChat();
  562. }
  563. }
  564. oldVideo = video;
  565. await delay(100);
  566. }
  567. })();
  568.  
  569. (async() => {
  570. let header = await checkForHTML('#masthead-container');
  571. header.addEventListener('focusin', e => {
  572. header.classList.add('show-header');
  573. });
  574. header.addEventListener('focusout', e => {
  575. header.classList.remove('show-header');
  576. });
  577. })();
  578.  
  579. let lastResize = 0;
  580.  
  581. async function setTopChat(currentResize = Date.now()) {
  582. let video = document.querySelector('ytd-watch-flexy:not([hidden]) #full-bleed-container video'),
  583. offline = document.querySelector('.ytp-offline-slate'),
  584. player = document.querySelector('#full-bleed-container');
  585. if(!(player && (video || offline))) return;
  586. if(!checkIfLiveChat()) {
  587. document.body.classList.remove('live-chat-top');
  588. }
  589. if(video && (!oldVideo || oldVideo.src != video.src)) {
  590. if(!video.videoWidth) {
  591. await new Promise(res => {
  592. video.addEventListener('loadedmetadata', res);
  593. });
  594. if(currentResize < lastResize) return;
  595. }
  596. }
  597. let videoHeight = video ? Math.round(video.videoHeight*window.innerWidth/video.videoWidth):720*window.innerWidth/1280,
  598. chatHeight = window.innerHeight - videoHeight,
  599. chatDocument = document.querySelector('#chatframe');
  600. chatDocument = chatDocument ? chatDocument.contentDocument:null;
  601. if(chatHeight >= 350) {
  602. for(let e of [document, chatDocument]) {
  603. if(!e) continue;
  604. e.body.classList.add('live-chat-top');
  605. e.documentElement.style.setProperty('--chat-height', chatHeight+'px');
  606. e.documentElement.style.setProperty('--video-height', videoHeight+'px');
  607. }
  608. } else {
  609. document.body.classList.remove('live-chat-top');
  610. chatDocument && chatDocument.body.classList.remove('live-chat-top');
  611. }
  612. //console.log('[PKGA Theater Mode] videoHeight', videoHeight, 'chatHeight', chatHeight);
  613. }
  614.  
  615. window.addEventListener('resize', async e => {
  616. let currentResize = Date.now();
  617. do {
  618. if(currentResize <= lastResize) break;
  619. else if(currentResize - lastResize > 100) {
  620. setTopChat(currentResize);
  621. lastResize = currentResize;
  622. break;
  623. } else await delay(currentResize - lastResize + 10);
  624. } while(true);
  625. });