NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name JungleTV Improvements // @namespace Violentmonkey Scripts // @match https://jungletv.live/ // @grant none // @version 2.6.1 // @author alexmangoman // @updateURL https://openuserjs.org/src/scripts/alexmangoman/JungleTV_Improvements.user.js // @icon https://jungletv.live/favicon.ico // @description Adds a few useful features to JungleTV // @license GPL-3.0-or-later; https://www.gnu.org/licenses/gpl-3.0.txt // ==/UserScript== let controlsOpen = false; let videoBrightness = 100; let monkeyAction = localStorage.getItem("monkeyAction"); if (monkeyAction == undefined) { localStorage.setItem("monkeyAction", "copy"); monkeyAction = "copy"; } let tipAmount = localStorage.getItem("tipAmount"); if (tipAmount == undefined) { localStorage.setItem("tipAmount", ""); tipAmount = ""; } //false by default let captchaAlert = localStorage.getItem("captchaAlert") === "true"; //keeps the interval id for the interval that checks if there's a captcha onscreen let captchaAlertInterval; //will be true while there is an unsolved captcha onscreen that a notification has already been sent for let captchaPending = false; function setCaptchaAlert(state) { localStorage.setItem("captchaAlert", state.toString()); } function sendCaptchaAlert() { captchaPending = true; const notification = new Notification('New Captcha', { body: 'A captcha has appeared on JungleTV', icon: 'https://jungletv.live/favicon.ico' }); notification.onclick = () => { window.focus(); notification.close(); } } function captchaAlertIntervalCallback() { if (document.querySelector("button[type=submit]")) { //if there's a captcha onscreen if (!captchaPending) { //and a notification hasn't already been sent sendCaptchaAlert(); } } else { captchaPending = false; } } function startCaptchaAlert() { //you can uncomment this code for it to work off-page, but it might mess with captchas and you should stay on the page anyway // //captchas won't display if you're off the page, so I can't detect them (unless I redefine document.hidden 😉) // Document.prototype.__defineGetter__("hidden", ()=>false); // Document.prototype.__lookupGetter__ = ()=>`function hidden() { // [native code] // }` // //they try to add iframes to get back the default document.hidden function, but I can change it to always return false before they read it // const oldAppend = Node.prototype.appendChild; // document.body.__proto__.appendChild = function(...args) { // let toReturn = oldAppend.call(this, ...args); // if (args[0].nodeName === "IFRAME") { // args[0].contentDocument.__defineGetter__("hidden",()=>false); // } // return toReturn; // } Notification.requestPermission().then(function (p) { if (p === 'granted') { new Notification('Notifications enabled!', { body: 'These will be sent when a captcha appears', icon: 'https://jungletv.live/favicon.ico' }); captchaAlertInterval = window.setInterval(captchaAlertIntervalCallback, 1000); } else { //uncheck the box and disable captcha alerts if notifications are blocked unsafeWindow.toggleCaptchaAlert(); document.getElementById("captchaAlert").checked = false; alert("Make sure you enable notifications if you want captcha notifications"); } }).catch(console.error); } if (captchaAlert) { startCaptchaAlert(); } function stopCaptchaAlert() { clearInterval(captchaAlertInterval); } if (localStorage.getItem("captchaAlert") == null) { setCaptchaAlert(false); } function copyTextToClipboard(text) { let textArea = document.createElement("textarea"); textArea.style.position = 'fixed'; textArea.style.top = "0"; textArea.style.left = "0"; textArea.style.width = '2em'; textArea.style.height = '2em'; textArea.style.padding = "0"; textArea.style.border = 'none'; textArea.style.outline = 'none'; textArea.style.boxShadow = 'none'; textArea.style.background = 'transparent'; textArea.value = text; document.body.appendChild(textArea); textArea.focus(); textArea.select(); let operation = document.execCommand('copy'); document.body.removeChild(textArea); let success = document.createElement("div"); success.textContent = "Copied!"; success.style.position = "fixed"; success.style.bottom = "10px" success.style.left = "50%"; success.style.transform = "translateX(-50%)"; success.style.backgroundColor = "rgb(30,255,30)"; success.style.color = "black"; success.style.borderRadius = "5px"; success.style.padding = "20px"; document.body.appendChild(success); setTimeout(()=>document.body.removeChild(success), 2500); } // replaces user's monkey icon onclick with one that copies their address (in the chat) function changeMonkeyClick(node) { if (node.nodeName === "#text") return; const image = Array.from(node.querySelectorAll('img')).filter(i=>i.src.includes("monkey"))[0]; //user avatar (filtered to ignore YT thumbnails) if (image) { const userAddress = image.src.slice(39, 103); const message = image.parentNode.parentNode.textContent; const dummy = document.createElement("div"); dummy.innerHTML = image.outerHTML; const newImage = dummy.firstChild; //have to re-make element to replace the click event newImage.title = "click for custom event"; newImage.onclick = () => { switch (monkeyAction) { case "copy": copyTextToClipboard(userAddress); break; case "bananovault": window.open(`https://vault.banano.cc/send?to=${userAddress}&amount=${tipAmount === "" ? window.prompt("How much BAN would you like to tip this user?") : tipAmount}`); break; case "kalium": window.open(`https://api.qrserver.com/v1/create-qr-code/?data=ban:${userAddress}?amount=${((tipAmount === "" ? window.prompt("How much BAN would you like to tip this user?") : tipAmount)*100000000000000000000000000000).toLocaleString('fullwide', {useGrouping:false})}`); break; case "creeper": window.open(`https://creeper.banano.cc/explorer/account/${userAddress}/history`); break; case "message": copyTextToClipboard(message); break; } } image.parentNode.replaceChild(newImage, image); } } const messageCallback = function(mutationsList, observer) { for (const mutation of mutationsList) { for (const addedNode of mutation.addedNodes) { changeMonkeyClick(addedNode); } } }; // run callback each time a message is sent const observer = new MutationObserver(messageCallback); //callback for checking the captcha alert box window.toggleCaptchaAlert = function () { const newCaptchaAlert = !captchaAlert; document.getElementById("captchaAlert").setAttribute("aria-checked", newCaptchaAlert) captchaAlert = newCaptchaAlert; setCaptchaAlert(newCaptchaAlert); newCaptchaAlert ? startCaptchaAlert() : stopCaptchaAlert(); } //callback for a change on the brightness slider window.hideVideo = function () { const brightness = document.getElementById('hideVideo').value; document.getElementById("brightness").textContent = `${brightness}%` document.getElementById('player').style.filter = `brightness(${brightness}%)`; } //callback for a change on the action select window.changeMonkeyAction = function () { const newMonkeyAction = document.getElementById('monkey').value; monkeyAction = newMonkeyAction; localStorage.setItem("monkeyAction", newMonkeyAction); } //callback for a change on the tip amount box window.changeTipAmount = function () { const newTipAmount = document.getElementById('tip').value; tipAmount = newTipAmount; localStorage.setItem("tipAmount", newTipAmount); } const controlsPanel = document.createElement("div"); controlsPanel.style.position = "fixed"; controlsPanel.style.top = "0"; controlsPanel.style.left = "0"; controlsPanel.style.width = "100vw"; controlsPanel.style.height = "100vh"; controlsPanel.style.background = "rgba(0,0,0,0.6)"; controlsPanel.style.display = "none"; controlsPanel.style.alignItems = "center"; controlsPanel.style.justifyContent = "center"; controlsPanel.style.zIndex = "10000"; controlsPanel.id = "controlsPanel"; controlsPanel.addEventListener("click", function(e) { e = window.event || e; if (this === e.target) { document.getElementById("controlsPanel").style.display = (controlsOpen = !controlsOpen) ? "flex" : "none"; } }); controlsPanel.innerHTML = ` <div style="color: white; padding: 20px; background: #111827; position: relative;"> <button style="position: absolute; top: 10px; right: 10px; font-size: 1.5rem; cursor: pointer; background: none; border: none; color: white;" id="closeMenu">✖</button> <h1 style="text-align: center; font-size: 2rem">Controls Panel</h1> <h5><b>Captcha Alerts</b></h5> <div> <label for="captchaAlert">Send a notification when a captcha appears (won't work if you're off the tab/page)</label><br> <div style="display: flex; align-items: center;"> OFF<button style="color: rgb(255, 255, 255); background-color: rgb(107, 114, 128); margin: 0 10px 0 10px" class="svelte-8jq8hp" role="switch" ${captchaAlert?"aria-checked='true'":""} onclick="toggleCaptchaAlert()" id="captchaAlert"></button>ON </div> </div> <h5 style="margin-top: 30px;"><b>Brightness</b></h5> <div> <label for="hideVideo">Change the brightness of the video</label><br> <div style="display: flex; align-items: center;"> <input style="margin-right: 10px;" type="range" min=0 max=100 value=100 oninput="hideVideo()" id="hideVideo" /><span id="brightness">100%</span> </div> </div> <h5 style="margin-top: 30px;"><b>MonKey Click</b></h5> <div> <label for="monkey">Change what happens when you click a chat message's monkey picture</label><br> <select id="monkey" style="color: black;" onchange="changeMonkeyAction()"> <option value="copy" ${monkeyAction==="copy"?"selected":""}>Copy user address</option> <option value="bananovault" ${monkeyAction==="bananovault"?"selected":""}>BananoVault transaction to user</option> <option value="kalium" ${monkeyAction==="kalium"?"selected":""}>Create scannable qr code</option> <option value="creeper" ${monkeyAction==="creeper"?"selected":""}>Open user account on creeper</option> <option value="message" ${monkeyAction==="message"?"selected":""}>Copy the message text</option> <option value="none" ${monkeyAction==="none"?"selected":""}>Do nothing</option> </select> </div> <h5 style="margin-top: 30px;"><b>Tip Amount</b></h5> <div> <label for="tip">How much BAN to tip a user. (Leave blank to bring up an input before tipping)</label><br> <input id="tip" type="number" placeholder="Type in a number..." style="color: black;" onChange="changeTipAmount()" value="${tipAmount}" /> </div> <h5 style="margin-top: 30px;"><b>Help</b></h5> <div> Is something not working the way it should? Try <a target="_blank" href="https://openuserjs.org/scripts/alexmangoman/JungleTV_Improvements">updating to the latest version</a>. <br> If this doesn't fix it, feel free to <a target="_blank" href="https://www.reddit.com/user/alexmangoman">DM me on Reddit</a>. </div> </div> `; document.body.appendChild(controlsPanel); const controlsToggleButton = document.createElement("button"); controlsToggleButton.onclick = () => document.getElementById("controlsPanel").style.display = (controlsOpen = !controlsOpen) ? "flex" : "none"; controlsToggleButton.textContent = "🎛️"; controlsToggleButton.style.display = "inline-block"; controlsToggleButton.style.fontSize = "30px"; controlsToggleButton.style.marginRight = "1rem"; controlsToggleButton.title = "toggle controls panel"; //start adding the custom onclick event for the MonKeys function loadMonKeyReplacer(n) { observer.disconnect(); //stop a buildup of observers if (document.querySelector("div.flex-grow:nth-child(1)")) { //if the chat is open observer.observe(document.querySelector("div.flex-grow:nth-child(1)"), {childList: true}); //listen for new chat messages document.querySelector("div.flex-grow:nth-child(1)").childNodes.forEach(changeMonkeyClick); //fix the MonKey for all the current chat messages } else { observer.observe(document.querySelector("div.overflow-x-hidden:nth-child(1)"), {childList: true}); //listen for new videos document.querySelector("div.overflow-x-hidden:nth-child(1)").childNodes.forEach(changeMonkeyClick); //fix the MonKey for all the current videos } } window.addEventListener("load", function () { //append the button to open the menu const navFlex = document.querySelector("a[href='/']").parentElement; navFlex.insertBefore(controlsToggleButton, navFlex.childNodes[1]); loadMonKeyReplacer(); //re-run when user changes tab (timeout is to account for animation) document.querySelector("div.h-9").childNodes[0].addEventListener("click",()=>setTimeout(loadMonKeyReplacer, 1000)); document.querySelector("div.h-9").childNodes[1].addEventListener("click",()=>setTimeout(loadMonKeyReplacer, 1000)); document.getElementById("closeMenu").onclick = () => document.getElementById("controlsPanel").style.display = (controlsOpen = !controlsOpen) ? "flex" : "none"; }); //All of the code below was written by u/banonkey // // (removed as JungleTV has implemented their own version of this) // // (function () { // function updateJungleBansPaid() { // const playerContainer = document.querySelector('.player-container'); // const sidebar = playerContainer.nextElementSibling; // const maxAge = 3600 * 2 * 1000; // const queueItems = sidebar.querySelectorAll( // ':scope > .transition-container > div > div > .flex-row' // ); // queueItems.forEach((item) => { // // Check if item has already been updated // const transactionsElement = item.querySelector( // ':scope .ban-transactions' // ); // if (transactionsElement !== null) { // return; // } // const itemTitle = item // ?.querySelector('.font-mono.cursor-pointer') // ?.getAttribute('title'); // if (itemTitle) { // // The complete BAN address // const banKey = itemTitle.replace('Click to copy: ', ''); // // API call to get last transactions // const url = // 'https://api.creeper.banano.cc/v2/accounts/' + banKey + '/history'; // fetch(url) // .then(function (response) { // if (response.status !== 200) { // console.log( // 'Looks like there was a problem. Status Code: ' + // response.status // ); // //return; // } // // Examine the text in the response // response.json().then(function (data) { // let sentBans = []; // data.forEach((transaction) => { // if (transaction.subtype === 'send') { // const amount = // Math.round( // transaction.amount / // 10000000000000000000000000 // ) / 10000; // const timeDifference = // Date.now() - transaction.timestamp; // if (timeDifference < maxAge) { // // console.log( // // 'Found Transaction: ' + // // amount + // // ' BAN ' + // // timeDifference + // // ' ago.' // // ); // sentBans = [...sentBans, amount]; // } // } // }); // // Append to queue item // const node = document.createElement('p'); // node.classList.add('ban-transactions', 'text-xs'); // node.style.color = 'red'; // node.style.fontWeight = 'bold'; // const textnode = document.createTextNode( // sentBans.filter(i=>i>=10).join(' or ')||"User paid with different address" //added a filter to ignore small tips sent from that account // ); // node.appendChild(textnode); // const col2 = item.querySelector( // ':scope .flex-col.flex-grow' // ); // col2.appendChild(node); // }); // }) // .catch(function (err) { // console.log('Fetch Error :-S', err); // }); // } // }); // } // window.setInterval(function () { // updateJungleBansPaid(); // }, 5000); // })();