// ==UserScript== // @name 4chan - YouTube Playlist // @description Wraps all YouTube videos inside a thread into a playlist // @namespace 4chan-yt-playlist // @version 2.4.9 // @include https://boards.4chan.org/*/thread/* // @include https://warosu.org/*/thread/* // @run-at document-start // @grant GM_setValue // @grant GM_getValue // ==/UserScript== const C = new class Conf { constructor() { this.initFinished = false; this.fourchan = location.hostname === "boards.4chan.org"; this.warosu = location.hostname === "warosu.org"; this.native = true; this.fixedNav = false; this.classicNav = false; this.autohideNav = false; this.fourchanX = false; const pathname = location.pathname.slice(1).split("/thread/"); this.board = pathname[0]; this.thread = pathname[1]; if (this.warosu) this.initFinished = true; document.addEventListener("4chanMainInit", () => { this.native = !unsafeWindow.Config.disableAll; this.fixedNav = unsafeWindow.Config.dropDownNav; this.classicNav = unsafeWindow.Config.classicNav; this.autohideNav = unsafeWindow.Config.autoHideNav; this.initFinished = true; }); document.addEventListener("4chanXInitFinished", () => { this.fourchanX = true; }); } get fixedHeader() { if (!this.fourchanX) return false; return document.documentElement.classList.contains("fixed"); } get autohideHeader() { if (!this.fourchanX) return false; return document.documentElement.classList.contains("autohide"); } }; class Dialog { constructor(playlist) { this.dragging = false; this.snapshot = { size: [0, 0], cursor: [0, 0], topbar: [0, 0, false, false] }; document.addEventListener("DOMContentLoaded", () => { document.body.insertAdjacentHTML("beforeend", "
×
"); const tabs = this.self?.querySelector("ul"); const reload = this.self?.querySelector(".reload"); const move = this.self?.querySelector(".move"); const jump = this.self?.querySelector(".jump"); const close = this.self?.querySelector(".close"); tabs.addEventListener("click", switchTab); reload.addEventListener("click", reloadPlaylist); move.addEventListener("mousedown", toggleDrag); jump.addEventListener("click", () => { playlist.jumpTo(playlist.track); }); close.addEventListener("click", () => { this.toggle(true); }); document.addEventListener("mouseup", toggleDrag); switch (true) { case C.fourchan: setPos(GM_getValue("4chan Dialog Coordinates", ["10%", "5%", null, null])); break; case C.warosu: setPos(GM_getValue("Warosu Dialog Coordinates", ["10%", "5%", null, null])); break; } function reloadPlaylist(e) { const icon = e.currentTarget?.firstElementChild; icon?.classList.add("spin"); return playlist.reload(); } }); document.addEventListener("DOMContentLoaded", () => { switch (true) { case C.fourchan: document.getElementById("navtopright")?.insertAdjacentHTML("afterbegin", "[Playlist] "); document.getElementById("navbotright")?.insertAdjacentHTML("afterbegin", "[Playlist] "); document.addEventListener("4chanParsingDone", () => { document.getElementById("settingsWindowLinkClassic")?.insertAdjacentHTML("beforebegin", "Playlist"); document.getElementById("settingsWindowLinkMobile")?.insertAdjacentHTML("beforebegin", "Playlist "); ["settingsWindowLinkClassic", "settingsWindowLinkMobile"].forEach((id) => { (document.getElementById(id)?.querySelector(".playlist-toggle")).onclick = initOrToggle; }); }); ["navtopright", "navbotright"].forEach((id) => { (document.getElementById(id)?.querySelector(".playlist-toggle")).onclick = initOrToggle; }); break; case C.warosu: const lastLink = document.getElementById("p" + C.thread)?.querySelector("a:last-of-type"); lastLink?.nextElementSibling?.insertAdjacentHTML("beforebegin", "[Playlist]"); document.querySelector(".playlist-toggle").onclick = initOrToggle; break; } }); document.addEventListener("4chanXInitFinished", () => { document.getElementById("shortcut-qr")?.insertAdjacentHTML("beforebegin", "YouTube Playlist"); document.querySelector(".playlist-toggle:not(.native)").onclick = initOrToggle; }); function switchTab(e) { if (!playlist.player) return; if (!e.target?.dataset.page) return; const index = e.target.dataset.page || "0"; playlist.player.cuePlaylist(playlist.toPages()[parseInt(index)]); } function initOrToggle() { if (playlist.checking || playlist.isEmpty()) return; if (!playlist.player) return initAPI(); return playlist.dialog?.toggle(); } function toggleDrag(e) { if (!playlist.dialog.self) return; switch (e.type) { case "mouseup": if (!playlist.dialog.dragging) return; playlist.dialog.dragging = false; document.removeEventListener("mousemove", moveDialog); GM_setValue((C.fourchan ? "4chan" : "Warosu") + " Dialog Coordinates", [ playlist.dialog.self.style.top, playlist.dialog.self.style.right, playlist.dialog.self.style.bottom, playlist.dialog.self.style.left ]); break; case "mousedown": if (e.button !== 0) return; e.preventDefault(); const rect = playlist.dialog.self.getBoundingClientRect(); playlist.dialog.snapshot.size[0] = rect.width; playlist.dialog.snapshot.size[1] = rect.height; playlist.dialog.snapshot.cursor[0] = e.x - rect.x; playlist.dialog.snapshot.cursor[1] = e.y - rect.y; if (C.fixedNav || C.fixedHeader) { const topbar = document.getElementById(C.fourchanX ? "header-bar" : C.classicNav ? "boardNavDesktop" : "boardNavMobile"); if (topbar) { playlist.dialog.snapshot.topbar[0] = topbar.getBoundingClientRect().y || 0; playlist.dialog.snapshot.topbar[1] = topbar.offsetHeight || 0; playlist.dialog.snapshot.topbar[2] = C.fourchanX ? C.fixedHeader : C.fixedNav; playlist.dialog.snapshot.topbar[3] = C.fourchanX ? C.autohideHeader : C.autohideNav; } } playlist.dialog.dragging = true; document.addEventListener("mousemove", moveDialog); break; } } function moveDialog(e) { if (!playlist.dialog.self) return; e.preventDefault(); const sW = document.documentElement.clientWidth, sH = document.documentElement.clientHeight, x = e.x - playlist.dialog.snapshot.cursor[0], y = e.y - playlist.dialog.snapshot.cursor[1], w = playlist.dialog.snapshot.size[0], h = playlist.dialog.snapshot.size[1], maxX = sW - w; let minY = 0, maxY = sH - h; if (playlist.dialog.snapshot.topbar[2]) { const [dialogPos, dialogHeight, fixed, autohide] = playlist.dialog.snapshot.topbar; if (fixed && !autohide) { if (dialogPos < dialogHeight) { minY += dialogHeight; } else { maxY -= dialogHeight; } } } if (y > maxY) { playlist.dialog.self.style.bottom = 0 + "px"; playlist.dialog.self.style.removeProperty("top"); } else { playlist.dialog.self.style.top = y > minY ? ((y * 100) / sH) + "%" : minY + "px"; playlist.dialog.self.style.removeProperty("bottom"); } if (x > maxX) { playlist.dialog.self.style.right = 0 + "px"; playlist.dialog.self.style.removeProperty("left"); } else { playlist.dialog.self.style.left = x > 0 ? ((x * 100) / sW) + "%" : 0 + "px"; playlist.dialog.self.style.removeProperty("right"); } } function initAPI() { if (playlist.state > 0 || playlist.player) return; if (playlist.state < 0) return failedLoad(); playlist.state = 1; const script = document.createElement("script"); script.src = "https://www.youtube.com/iframe_api"; document.head.appendChild(script); setTimeout(() => { if (playlist.state < 1) return; playlist.state = -1; failedLoad(); }, 5000); script.onerror = () => { failedLoad(); playlist.state = -1; }; function failedLoad() { const msg = "Unable to load YouTube Iframe API.\nPress F12 and follow the instructions in the console."; if (!C.fourchanX) alert(msg); document.dispatchEvent(new CustomEvent("CreateNotification", { detail: { type: "error", content: msg } })); console.info("Unable to load YouTube Iframe API\n\n" + "4chanX's Settings > Advanced > Javascript Whitelist\n\n" + " https://www.youtube.com/iframe_api\n" + " https://www.youtube.com/s/player/" + "\n\nFilters in your AdBlock extension\n\n" + " @@||www.youtube.com/iframe_api$script,domain=4chan.org\n" + " @@||www.youtube.com/s/player/*$script,domain=4chan.org\n"); } } function setPos(coordinates) { coordinates.forEach((pos, index) => { if (!pos || !playlist.dialog?.self) return; switch (index) { case 0: playlist.dialog.self.style.top = pos; break; case 1: playlist.dialog.self.style.right = pos; break; case 2: playlist.dialog.self.style.bottom = pos; break; case 3: playlist.dialog.self.style.left = pos; break; } }); } } get self() { return document.getElementById("playlist-embed"); } get toggleBtn() { return document.querySelector(".playlist-toggle:not(.native)"); } toggle(close) { if (close || !this.self?.classList.contains("hide")) { this.self?.classList.add("hide"); if (!C.fourchanX) return; const button = document.getElementById("shortcut-playlist")?.firstElementChild; button?.classList.add("disabled"); } else { this.self?.classList.remove("hide"); if (!C.fourchanX) return; const button = document.getElementById("shortcut-playlist")?.firstElementChild; button?.classList.remove("disabled"); } } updateTabs(amount) { const tabs = this.self?.querySelector("ul"); if (!tabs) return console.error("No