HoloTools Comment Widener

Puts comments next to videos instead of inside of their boxes

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Advertisement:

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

Advertisement:

// ==UserScript==
// @name         HoloTools Comment Widener
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  Puts comments next to videos instead of inside of their boxes
// @author       TellowKrinkle
// @match        https://hololive.jetri.co/
// @grant        none
// ==/UserScript==

(function() {
	'use strict';

	const videoPadding = 3; // px
	const containerPadding = 5; // px
	const commentWidth = 300; // px

	/** Check if HoloTools is in auto layout mode */
	function isAutoLayout() {
		let layoutMode = document.getElementsByClassName("layout-mode")[0];
		if (!layoutMode) { return true; } // If holotools updates and this breaks, say yes
		return layoutMode.innerHTML !== "unfold_more";
	}

	/**
	 * Helper for mass update of HTML nodes
	 * For each (string, function) pair, calls function on all elements with the class string
	 * @param {Object.<string, function(HTMLElement):void>} updates
	 */
	function updateByClass(updates) {
		for (const className of Object.keys(updates)) {
			for (const element of document.getElementsByClassName(className)) {
				updates[className](element);
			}
		}
	}

	/** Resize all videos to the given width */
	function resizeVideos(videoWidth, containerHeight, hasComments) {
		const height = videoWidth * 9 / 16;
		const containerWidth = hasComments ? videoWidth + 300 : videoWidth;
		updateByClass({
			"player-yt-box": (container) => {
				container.style.width = `${containerWidth}px`;
				container.style.height = `${containerHeight}px`;
			},
			"player-yt-frame": (video) => {
				video.style.top = "0px";
				video.children[0].style.top = "0px";
				video.children[0].style.width = `${videoWidth}px`;
				video.children[0].style.height = `${height}px`;
			},
			"player-yt-info": (info) => { info.style.display = "none"; }
		});
	}

	/** Calculates the size of the video and its player */
	function calculateVideoSize(videoCount, videosWide, containerWidth, containerHeight, hasComments) {
		const videosHigh = Math.ceil(videoCount / videosWide);
		const widthPadding = videosWide * videoPadding;
		const heightPadding = videosHigh * videoPadding;
		const videoWidth = Math.floor((containerWidth - widthPadding) / videosWide) - (hasComments ? commentWidth : 0);
		const videoHeight = Math.floor((containerHeight - heightPadding) / videosHigh);
		return {width: Math.min(videoWidth, Math.floor(videoHeight * 16 / 9)), commentHeight: videoHeight};
	}

	/** Calculates the optimal number of videos wide to get the best usage of space */
	function calculateOptimalVideosWide(videoCount, containerWidth, containerHeight, hasComments) {
		let best = calculateVideoSize(videoCount, 1, containerWidth, containerHeight, hasComments).width;
		for (let videosWide = 2; videosWide < 10; videosWide++) {
			let newHeight = calculateVideoSize(videoCount, videosWide, containerWidth, containerHeight, hasComments).width;
			if (newHeight > best) {
				best = newHeight;
			} else {
				return videosWide - 1;
			}
		}
		throw `calculateOptimalVideosWide broke or there were so many videos that the optimal layout was 10 or more wide`;
	}

	let lastVideoWidth = 0; // To track whether we need to rerun relayoutVideos

	/** Recalculates the proper video size and applies it */
	function relayoutVideos() {
		const container = document.getElementsByClassName("player-container")[0];
		const videoCount = container.children.length;
		const containerWidth = container.clientWidth - containerPadding * 2;
		const containerHeight = container.clientHeight - containerPadding * 2;
		const hasComments = document.getElementsByClassName("withChat").length !== 0;
		const currentVideosWide = /repeat\((\d+)/.exec(container.style["grid-template-columns"])[1] | 0;
		let videosWide = currentVideosWide;
		if (isAutoLayout()) {
			videosWide = calculateOptimalVideosWide(videoCount, containerWidth, containerHeight, hasComments);
		}
		if (videosWide !== currentVideosWide) {
			container.style["grid-template-columns"] = `repeat(${videosWide}, 1fr)`;
		}
		const videoSize = calculateVideoSize(videoCount, videosWide, containerWidth, containerHeight, hasComments);
		console.log(`Will resize videos to ${videoSize.width}px wide`);
		lastVideoWidth = videoSize.width;
		resizeVideos(videoSize.width, videoSize.commentHeight, hasComments);
	}

	// In case someone wants to manually call it
	window.relayoutVideos = relayoutVideos;

	const videoResizeObserver = new MutationObserver(function(records) {
		for (const record of records) {
			if (record.attributeName !== "style" && record.attributeName !== "class") { continue; }
			if (record.target.style.width !== lastVideoWidth + "px") {
				relayoutVideos();
				return;
			}
		}
	});

	/** @param {HTMLElement} element */
	function watchPlayerResize(element) {
		videoResizeObserver.observe(element.children[0], {attributes: true});
		for (const child of element.children[0].children) {
			for (const grandchild of child.children) {
				if (grandchild.nodeName === "IFRAME") {
					videoResizeObserver.observe(grandchild, {attributes: true});
				}
			}
		}
	}

	/** @param {HTMLElement} element */
	function watchVideoAddRemove(element) {
		const videoAddRemoveObserver = new MutationObserver(function(records) {
			for (const record of records) {
				for (const node of record.addedNodes) {
					watchPlayerResize(node);
				}
			}
		});

		for (const child of element.children) { watchPlayerResize(child); }
		videoAddRemoveObserver.observe(element, {childList: true});
	}

	/** Initializes the userscript */
	function setup() {
		// Stores the currently-being-watched object so we don't double-watch it
		let currentWatch = document.getElementsByClassName("player-container")[0];
		if (currentWatch) { watchVideoAddRemove(currentWatch); }
		const observer = new MutationObserver(function() {
			let playerContainer = document.getElementsByClassName("player-container");
			if (currentWatch !== playerContainer[0]) {
				currentWatch = playerContainer[0];
				if (playerContainer[0]) {
					watchVideoAddRemove(playerContainer[0]);
				}
			}
		});

		const mainApp = document.getElementsByClassName("md-content")[0];
		observer.observe(mainApp, {childList: true});
	}

	// Run on page load
	if (document.readyState !== "loading") {
		setup();
	} else {
		document.addEventListener("DOMContentLoaded", setup);
	}
})();