|
// ==UserScript== |
|
// @name NPO Series Player |
|
// @namespace http://tampermonkey.net/ |
|
// @version 2024-07-16 |
|
// @description While watching a series, make the player full-width. |
|
// @author You |
|
// @match https://npo.nl/start/serie/* |
|
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== |
|
// @grant GM_addStyle |
|
// ==/UserScript== |
|
|
|
// TODO: when a new view starts on the existing page (automatically moving to the next episode), |
|
// set up the video player again |
|
|
|
(function() { |
|
'use strict'; |
|
|
|
let hasStartedUp = false; |
|
let showSidebar = false; |
|
let desiredPlaybackRate = 1.0; |
|
|
|
function uiIsPresent() { |
|
return !!document.querySelector('[class^="PlayerSidebarContainer_PlayerSidebarContainer"],[class*=" PlayerSidebarContainer_PlayerSidebarContainer"]') |
|
} |
|
|
|
function startUI() { |
|
// The element has a class name like "PlayerSidebarContainer_PlayerSidebarContainer__RANDOMSHIT" |
|
const sidebar = document.querySelector('[class^="PlayerSidebarContainer_PlayerSidebarContainer"],[class*=" PlayerSidebarContainer_PlayerSidebarContainer"]'); |
|
if (!sidebar) { return; } |
|
|
|
GM_addStyle(` |
|
/* Don't show the shitty controls that NPO layers on top */ |
|
.bmpui-ui-controlbar, .bmpui-ui-container, .bmpui-ui-titlebar { |
|
display: none !important; |
|
} |
|
.bmpui-ui-uicontainer .bmpui-ui-subtitle-overlay, .bmpui-ui-uicontainer .bmpui-ui-subtitle-overlay.bmpui-controlbar-visible { |
|
/* Don't move the subtitles up when the video is paused */ |
|
bottom: 40px !important; |
|
/* Don't disable interaction with the subtitle text. */ |
|
pointer-events: all !important; |
|
} |
|
.playback-controls { |
|
display: flex; |
|
justify-content: center; |
|
gap: 3em; |
|
} |
|
/* Style the controls we're adding to the page. */ |
|
button.tm-set-playback-rate { |
|
padding: 0.7em; |
|
background-color: rgba(255,255,255,0.1); |
|
color: white; |
|
font-weight: bold; |
|
border: 1px solid #ccc; |
|
border-radius: 8px; |
|
} |
|
button.tm-set-playback-rate.selected { |
|
color: rgb(255, 109, 0); |
|
} |
|
`) |
|
|
|
// The player's container has no unique class at all |
|
const playerContainer = sidebar.nextElementSibling; |
|
|
|
const contentContainer = sidebar.parentElement.parentElement; |
|
|
|
if (playerContainer) { |
|
// Make the video full-width |
|
playerContainer.className = "col-span-12"; |
|
} |
|
|
|
if (contentContainer) { |
|
// Move the sidebar content (list of episodes) to under the episode description. |
|
contentContainer.appendChild(sidebar); |
|
} |
|
|
|
const playerInfo = playerContainer.querySelector('[data-testid="player-info"]'); |
|
|
|
if (playerInfo) { |
|
const playbackControls = document.createElement('div'); |
|
playbackControls.className = 'playback-controls'; |
|
const playbackControlButtons = [ |
|
makeSkipButton(-2), |
|
makeSkipButton(-5), |
|
makeRateChangeButton(0.675), |
|
makeRateChangeButton(0.75), |
|
makeRateChangeButton(0.825), |
|
makeRateChangeButton(0.9), |
|
makeRateChangeButton(1.0), |
|
]; |
|
playbackControls.append(...playbackControlButtons); |
|
playerInfo.insertAdjacentElement('beforebegin', playbackControls); |
|
} |
|
|
|
// When space is pressed, pause/unpause the video instead of paging down. |
|
// This is super necessary since the the subtitles can be interacted with, clicking the video will not play/pause. |
|
document.addEventListener('keydown', function(e) { |
|
if (e.code == "Space") { |
|
e.preventDefault(); |
|
const video = document.querySelector('video'); |
|
if (video?.paused) { |
|
video.play(); |
|
} else { |
|
video.pause(); |
|
} |
|
} |
|
}); |
|
} |
|
|
|
function makeRateChangeButton(rate) { |
|
const element = document.createElement('button'); |
|
element.onclick = function() { |
|
setVideoPlaybackRate(rate); |
|
} |
|
element.className = 'tm-set-playback-rate'; |
|
element.setAttribute('data-playback-rate', rate); |
|
element.appendChild(document.createTextNode(`${rate} X`)); |
|
return element; |
|
} |
|
|
|
function setVideoPlaybackRate(rate) { |
|
document.querySelector(`.tm-set-playback-rate.selected`)?.classList?.remove('selected'); |
|
document.querySelector(`.tm-set-playback-rate[data-playback-rate="${rate}"]`)?.classList?.add('selected'); |
|
document.querySelector('video').playbackRate = rate; |
|
} |
|
|
|
function makeSkipButton(distance) { |
|
const element = document.createElement('button'); |
|
element.onclick = function() { |
|
skipVideo(distance); |
|
} |
|
element.className = 'tm-skip-video'; |
|
element.setAttribute('data-skip-distance', distance); |
|
element.appendChild(document.createTextNode(`Skip ${distance}`)) |
|
return element; |
|
} |
|
|
|
function skipVideo(distance) { |
|
const video = document.querySelector('video'); |
|
video.currentTime = video.currentTime + distance |
|
} |
|
|
|
let startupTimer = window.setInterval(startup, 100); |
|
|
|
function startup() { |
|
if (!uiIsPresent()) { |
|
return; |
|
} |
|
|
|
window.clearInterval(startupTimer); |
|
|
|
startUI(); |
|
setUpVideoControls(); |
|
hasStartedUp = true; |
|
} |
|
|
|
function videoIsPresent() { |
|
return !!document.querySelector('video'); |
|
} |
|
|
|
let videoHasStarted = false; |
|
let videoSetupTimer = window.setInterval(videoStartup, 100); |
|
|
|
function videoStartup() { |
|
if (!videoIsPresent()) { |
|
return; |
|
} |
|
|
|
window.clearInterval(videoSetupTimer); |
|
|
|
// Once the video appears, wait for it to start playing. That is done by the overlay control provided by NPO. |
|
const video = document.querySelector('video'); |
|
video.addEventListener('play', setUpVideoControls); |
|
video.addEventListener('pause', fixupSubtitles); |
|
} |
|
|
|
function setUpVideoControls() { |
|
// Once the overlay control is used, hide the ugly, useless thing. |
|
document.querySelector('.bmpui-ui-playbacktoggle-overlay').style.display = 'none'; |
|
// Show the standard video controls. |
|
document.querySelector('video').setAttribute('controls', 'true'); |
|
|
|
// Turn on subtitles by clicking the control. NPO doesn't remember whether they were turned on. |
|
document.querySelector('button[aria-label="Nederlands"]').click() |
|
|
|
// To ensure the correct button is highlighted. |
|
// TODO: remember the preference and keep it. |
|
setVideoPlaybackRate(1.0); |
|
|
|
const video = document.querySelector('video'); |
|
video.removeEventListener('play', setUpVideoControls); |
|
} |
|
|
|
function fixupSubtitles() { |
|
const parent = document.querySelector('.bmpui-ui-subtitle-label'); |
|
parent.style.whiteSpace = 'preserve-breaks'; |
|
|
|
// Replace <br> tags with a span containing a linefeed character. |
|
// This allows screen readers and translation tools to see the lines as part of the same sentence/clause. |
|
const breaks = parent.querySelectorAll('br'); |
|
breaks.forEach((br) => { |
|
const newlineSpan = document.createElement('span'); |
|
newlineSpan.textContent = "\x0a"; |
|
br.replaceWith(newlineSpan); |
|
}); |
|
} |
|
|
|
})(); |