HoloTools Comment Widener

Puts comments next to videos instead of inside of their boxes

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

Advertisement:

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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);
	}
})();