alexmangoman / JungleTV Improvements

// ==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);

// })();