FFProgs

Adds MultiStream Vod Watching and a Button to view logs in xivanalysis also some minor improvements.

目前为 2022-04-12 提交的版本。查看 最新版本

// ==UserScript==
// @name                FFProgs
// @name:en             FFProgs
// @name:ja             FFプログレス
// @namespace           k_fizzel
// @version             1.0.0
// @author              k_fizzel
// @description         Adds MultiStream Vod Watching and a Button to view logs in xivanalysis also some minor improvements.
// @description:en      Adds MultiStream VOD Watching and a Button to view logs in xivanalysis also some minor improvements.
// @description:ja      マルチストリームVODウォッチングとxivanalysisでログを表示するためのボタンもいくつかのマイナーな改善を追加します。
// @website             https://www.fflogs.com/character/id/12781922
// @icon                https://assets.rpglogs.com/img/ff/favicon.png?v=2
// @match               https://www.fflogs.com/*
// @match               https://*.fflogs.com/*
// @grant               GM_addStyle
// @grant               unsafeWindow
// @license             MIT License
// ==/UserScript==

/*
To Do:
refactor code to make it look better pick a standard and use es6 functions and jquery
right click a log in the encounters page to highlight with a specific color it so that it stands out

pause video when it gets to the end of a replay
have global event listeners instead of a bunch of click ones
make the video player stay in the same x y on log pull change
Export current vod data so that you can import it
when you share a twitch/youtube url make the timestamp the start
make offset work with decimals
Make youtube player work and private youtube streams as well
github once I get to 1.0.0 with a readme on how to install delete this current git repo

Maybe:
add keybindings
make the player keep the same aspect ratio
make player start at a larger size
greasy fork pull from github for updates


 */
(function () {

  // Helper functions
  function addGlobalEventListener(type, selector, callaBack) {
    document.addEventListener(type, e => {
      if (e.target.matches(selector)) callaBack(e);
    })
  }

  // Adblock.
  $("#top-banner, #bottom-banner, #playwire-video-container, #patron-box, #gear-box-ad").remove();

  // Remove alt-text from item images. (Alt text looks awful when api is not up to date and relics )
  $(".table-icon").removeAttr("alt");

  // Adds xivanalysis button.
  const updateXIVAnalysisUrl = () => { $("#xivanalysis-tab").attr("href", `https://xivanalysis.com/report-redirect/${location.href}`); }
  $("#filter-analyze-tab").before(`<a href="https://xivanalysis.com/report-redirect/${location.href}" target="_blank" class="big-tab view-type-tab" id="xivanalysis-tab"><span class="zmdi zmdi-time-interval"></span> <span class="big-tab-text"><br>xivanalysis</span></a>`)
  $("#xivanalysis-tab").click(updateXIVAnalysisUrl);

  // Checks if video player button exists
  const videoButton = document.querySelector(".replay-video");
  if (/\/reports\/.+/.test(location.pathname)) {
    const streams = {
      // test data
      "Chad_Bradly": { platform: 1, url: 'https://www.twitch.tv/videos/787291919?t=01h04m19s', offset: -1312 },
      "Charlie_Cerise": { platform: 2, url: 'https://www.youtube.com/watch?v=d_BBgHPIlWg' },
      "Gale_Eternia": { platform: 2, url: 'https://www.youtube.com/watch?v=d_BBgHPIlWg' },
      "Glyphimor_Epsilon": { platform: 1, url: 'https://www.twitch.tv/videos/787291919?t=01h04m19s' },
      "Kara_Doomfist": { platform: 2, url: 'https://www.youtube.com/watch?v=d_BBgHPIlWg' },
      "M'aique_Delieur": { platform: 1, url: 'https://www.twitch.tv/videos/787291919?t=01h04m19s' },
      "Nishi_Michu": { platform: 2, url: 'https://www.youtube.com/watch?v=d_BBgHPIlWg' },
      "Pual_Pual": { platform: 1, url: 'https://www.twitch.tv/videos/787291919?t=01h04m19s' }
    }
    let person, videoFrame, videoPlayer, allowPlay = false, timesUpdate = 0, videPlayerOpen = false, multiStreamOpen = false, multiStreamMovement = false, isDragging = false;

    // iframe api's
    const firstScriptTag = $("script")[0]
    $(`<script src="https://player.twitch.tv/js/embed/v1.js"></script>
    <script src="https://www.youtube.com/iframe_api"></script>`).insertBefore(firstScriptTag);

    // doesn't work with jquery LMAO
    const interactModule = document.createElement("script");
    interactModule.type = "module";
    interactModule.innerHTML = `    
    import interact from "https://cdn.interactjs.io/v1.10.11/interactjs/index.js";
    interact("#video-frame").resizable({
      edges: { left: true, right: true, bottom: true, top: true },
      listeners: {
        move (event) {
          let target = event.target;
          let x = (parseFloat(target.getAttribute("data-x")) || 0);
          let y = (parseFloat(target.getAttribute("data-y")) || 0);

          target.style.width = event.rect.width + "px";
          target.style.height = event.rect.height + "px";

          x += event.deltaRect.left;
          y += event.deltaRect.top;

          target.style.transform = \`translate(\${x}px, \${y}px)\`;

          target.setAttribute("data-x", x);
          target.setAttribute("data-y", y);
        }
      },
      inertia: true,
      
      modifiers: [
        interact.modifiers.aspectRatio({
          // make sure the width is always double the height
          ratio: 1.72,

          modifiers: [
            interact.modifiers.restrictRect({ endOnly: true }),
          ],
        }),
        interact.modifiers.restrictSize({
          min: { width: 560, height: 337 }
        })
      ]
    })
    .draggable({
      listeners: {
        move (event) {
          let target = event.target;
          
          let x = (parseFloat(target.getAttribute("data-x")) || 0) + event.dx;
          let y = (parseFloat(target.getAttribute("data-y")) || 0) + event.dy;

          
          target.style.transform = \`translate(\${x}px, \${y}px)\`;

          target.setAttribute("data-x", x);
          target.setAttribute("data-y", y);
        },
      },
      inertia: true,
      modifiers: [
        interact.modifiers.restrictRect({
          restriction: "body",
          endOnly: true
        })
      ]
    })`;
    firstScriptTag.parentNode.insertBefore(interactModule, firstScriptTag)

    // updates the position of the video to match the log
    const updatePosition = () => {
      const timeSinceFirstPull = parseInt(streams[person].offset) + ((unsafeWindow.replayPosition - unsafeWindow.fights[0].start_time) / 1000);
      videoPlayer.seek(timeSinceFirstPull)
    }

    // returns if the player is allows to play
    const togglePlay = () => {
      allowPlay = !allowPlay;
      return !allowPlay;
    }

    // make the log play button play the video
    $("#play-button").attr("onclick", "toggleReplayState(this)").click((e) => {
      if (videoPlayer.isPaused()) {
        videoPlayer.play();
        togglePlay();
      } else {
        videoPlayer.pause();
        togglePlay();
      }
    });
    // updates video when moving via the replay bar

    $("#graph").on("mousedown", (e) => {
      if (e.type == "mousedown") {
        isDragging = true;
        updatePosition();
      }
    });
    $("body").on("mousemove mouseup", (e) => {
      if (videPlayerOpen && multiStreamOpen) {
        if (e.type == "mouseup") {
          if (isDragging) {
            isDragging = false;
            updatePosition();
          }
        }
      }
    })

    function stopLoading() {
      const loading = document.getElementById("multistream-loading-icon")
      if (loading) {
        loading.outerHTML = "";
      }

      const multistreamPlayer = document.getElementById("multistream_player");
      multistreamPlayer.style.display = "block";
    }

    function onTwitchPlayerReady() {
      updatePosition();
      videoPlayer.play()
      stopLoading();
    }

    function onTwitchPlayerEnded() {

    }
    function onTwitchPlayerPlaying() {
      if (!allowPlay) videoPlayer.pause()

    }
    function onTwitchPlayerPause() {
      if (allowPlay) videoPlayer.play()
    }
    function onTwitchPlayerSeek() {

    }

    function onYoutubePlayerReady(e) {
      stopLoading();

    }

    function onYoutubePlayerStateChange(e) {
      switch (e) {
        case 0: //onYouTubePlayerEnded

          break;
        case 1: //onYouTubePlayerPlaying

          break;
        case 2: //onYouTubePlayerPaused

          break;
        case 3: //onYouTubePlayerBuffering

          break;
        case 5: //onYouTubePlayerVideoCued

          break;

        default:
          break;
      }
    }

    function showMultiStreamPlayer() {
      if ($("#video-frame").css("display") === "block") {
        videoFrame = document.getElementById("video-frame-inner");
        videoFrame.innerHTML = "<div style='text-align:center; margin-top:100px' id='multistream-loading-icon'><p>Loading Video...</p><p><img src='https://assets.rpglogs.com/img/spinny.gif'></p></div>";
        let playerDiv = document.createElement("div");
        playerDiv.id = "multistream_player";
        videoFrame.innerHTML += playerDiv.outerHTML;
        playerDiv = document.getElementById("multistream_player");
        playerDiv.style = "width: 100%; height: 100%; display: none;";
        // If noddy is selected play fist stream;

        const stream = streams[person];
        const videoID = new URL(stream.url)
        if (stream.platform === 1) {
          videoPlayer = new Twitch.Player("multistream_player", {
            width: "100%",
            height: "100%",
            autoplay: false,
            muted: true,
            video: videoID.pathname.split("/")[2],
          });
          videoPlayer.addEventListener(Twitch.Player.READY, onTwitchPlayerReady);

          videoPlayer.addEventListener(Twitch.Player.ENDED, onTwitchPlayerEnded);
          videoPlayer.addEventListener(Twitch.Player.PLAYING, onTwitchPlayerPlaying);
          videoPlayer.addEventListener(Twitch.Player.PAUSE, onTwitchPlayerPause);
          videoPlayer.addEventListener(Twitch.Player.SEEK, onTwitchPlayerSeek);
        }


        if (stream.platform === 2) {
          videoPlayer = new YT.Player("multistream_player", {
            height: "100%",
            width: "100%",
            videoId: videoID.searchParams.get("v"),
            playerVars: {
              controls: 0
            }
          })
          videoPlayer.addEventListener("onReady", onYoutubePlayerReady);
          videoPlayer.addEventListener("onStateChange", onYoutubePlayerStateChange);
        }
      }

    }

    function toggleMultiStreamPlayer() {
      multiStreamOpen = !multiStreamOpen

      updateMultistreamButton()
      if (multiStreamOpen) {
        showMultiStreamPlayer()
      }
    }

    function updateMultistreamButton() {
      const multiStreamViewButton = document.getElementById("multistream-view")
      if (multiStreamOpen) {
        multiStreamViewButton.style.backgroundColor = "#000060"
      } else {
        multiStreamViewButton.style.backgroundColor = ""
      }
    }

    function showMultistreamOptions() {
      if (multiStreamOpen) {
        toggleMultiStreamPlayer()
      }
      videoFrame = document.getElementById("video-frame-inner");
      addGlobalEventListener("input", ".url-table-row", (e) => {
        const [user, action] = e.target.id.split("-");
        const value = e.target.value;
        if (action === "stream_url") {
          let url, platform;
          let err = false;
          try {
            url = new URL(value);
            if (url.hostname === "www.twitch.tv" && url.pathname.match(/\/videos\/\d+/g)) {
              platform = 1;
            } else if (url.hostname === "www.youtube.com" && url.pathname === "/watch" && url.searchParams.get("v")) {
              platform = 2;
            } else {
              throw "not a valid platform"
            }
          }
          catch (error) {
            if (streams[user]) {
              delete streams[user];
            }
            if (error) err = true;
          }
          finally {
            if (!err) {
              if (!streams[user]) streams[user] = {};
              streams[user].platform = platform;
              streams[user].url = url.href;
            }
          }
        }
        if (action === "stream_offset") {
          if (streams[user] && streams[user].url) {
            let offset = parseInt(value);
            streams[user].offset = offset;
          }
        }
      })

      const div = document.createElement("div");
      div.style = "text-align: center;";

      const form = document.createElement("form");

      form.style = "margin: 0;";
      form.acceptCharset = "utf-8";
      form.method = "GET";
      form.action = "javascript:void(0);";

      const table = document.createElement("table");
      table.style = "border-collapse: separate; border-spacing: 8px; margin: auto; text-align: left;";

      //Table Information
      const infoRow = document.createElement("tr");
      const nameInfo = document.createElement("td");
      const URLInfo = document.createElement("td");
      const offsetInfo = document.createElement("td");
      nameInfo.innerHTML = "Name:";
      URLInfo.innerHTML = "Stream URL:";
      offsetInfo.innerHTML = "Offset:";
      offsetInfo.style = "max-width: 60px;";

      infoRow.append(nameInfo, URLInfo, offsetInfo);
      table.appendChild(infoRow);

      const raidFrames = document.querySelectorAll(".raid-frame-contents");
      const raiders = [];
      raidFrames.forEach((frame) => {
        raiders.push(frame.innerHTML);
      })

      raiders.forEach((person) => {
        person = person.replace(" ", "_");

        const tableRow = document.createElement("tr");
        const nameData = document.createElement("td");
        nameData.innerHTML = person;
        tableRow.appendChild(nameData);
        const streamData = document.createElement("td");
        const streamUrl = document.createElement("input");
        streamUrl.style = "min-width: 260px;"
        streamUrl.type = "url";
        streamUrl.id = `${person}-stream_url`;
        streamUrl.name = `${person}-stream_url`;
        streamUrl.placeholder = "youtube.com/watch?v= or twitch.tv/videos/";
        if (streams[person] && streams[person].url) {
          streamUrl.setAttribute("value", streams[person].url);
        }
        streamUrl.classList.add("url-table-row");
        streamData.appendChild(streamUrl);
        tableRow.appendChild(streamData);

        const offsetData = document.createElement("td");
        const offsetTime = document.createElement("input");
        offsetTime.type = "number";
        offsetTime.style = "width: 5em;";
        offsetTime.id = `${person}-stream_offset`;
        offsetTime.name = `${person}-stream_offset`;
        offsetTime.placeholder = "# in sec";
        if (streams[person] && streams[person].offset) {
          offsetTime.setAttribute("value", streams[person].offset)
        }
        offsetTime.classList.add("url-table-row");

        offsetData.appendChild(offsetTime);
        tableRow.appendChild(offsetData);

        table.appendChild(tableRow);
      })

      form.appendChild(table);
      div.appendChild(form);
      videoFrame.innerHTML = div.outerHTML;
    }

    function toggleMultiStreamMove() {
      multiStreamMovement = !multiStreamMovement;
      const frameInner = document.getElementById("video-frame-inner");
      const resizeButton = document.getElementById("multistream-resize");
      if (multiStreamMovement) {
        resizeButton.style.backgroundColor = "#000060"
        frameInner.style.pointerEvents = "none"
      } else {
        resizeButton.style.backgroundColor = ""
        frameInner.style.pointerEvents = ""
      }
    }
    let prvPerson;
    document.addEventListener("click", (e) => {
      let selected = document.querySelector(".raid-frame.selected .raid-frame-contents");
      (selected) ? person = selected.innerHTML.replace(" ", "_") : person = Object.keys(streams)[0];
      if (prvPerson !== person) {
        if (multiStreamOpen) {
          showMultiStreamPlayer();
        }
      }
      prvPerson = person;
    })

    const positionGraph = document.getElementById("position-graph");
    const observer = new MutationObserver(function () {
      timesUpdate++;

      if (timesUpdate > 2) {
        if (!positionGraph.style.opacity) {
          const videoButton = document.querySelector(".replay-video");
          if (videPlayerOpen) {
            allowPlay = false;
            videoButton.click()
            videoButton.click()
            updateMultistreamButton()
            toggleMultiStreamMove()
            toggleMultiStreamMove()
          }
        }
      }
    });
    observer.observe(positionGraph, { attributes: true });

    videoButton.addEventListener("click", (e) => {
      $("#fullscreen-video, #remove-video, #select-video, #mute-video").remove();

      const frame = document.getElementById("video-frame");
      frame.style.touchAction = "none";
      frame.style.boxSizing = "border-box"


      videoFrame = document.getElementById("video-frame-inner");
      videPlayerOpen = !videPlayerOpen
      if (multiStreamOpen) {
        setTimeout(showMultiStreamPlayer, 500)
      }
      // Creates Menu Below Video Player
      const multiStreamOptions = document.getElementById("multistream-options");
      if (!multiStreamOptions) {

        const videoFrameControls = document.getElementById("video-frame-controls");
        videoFrameControls.style.cursor = "ns-resize"
        const multiStreamO = document.createElement("span");
        multiStreamO.style = "float: right; cursor: pointer;";
        multiStreamO.id = "multistream-options";
        multiStreamO.classList.add("graph-legend-button");
        multiStreamO.onclick = showMultistreamOptions;
        multiStreamO.innerText = "Multistream options"
        videoFrameControls.appendChild(multiStreamO);

        const multiStreamV = document.createElement("span");
        multiStreamV.style = "margin-right: -1px; float: right; cursor: pointer;";
        multiStreamV.id = "multistream-view";
        multiStreamV.classList.add("graph-legend-button");
        multiStreamV.onclick = toggleMultiStreamPlayer;
        multiStreamV.innerText = "Multistream View";
        videoFrameControls.appendChild(multiStreamV);

        const resizeButton = document.createElement("span");
        resizeButton.style = "float: left; cursor: pointer;";
        resizeButton.id = "multistream-resize";
        resizeButton.classList.add("graph-legend-button");
        resizeButton.onclick = toggleMultiStreamMove;
        resizeButton.innerText = "Resize/Move";
        videoFrameControls.appendChild(resizeButton);
      }
    })
  }

  // Checks if it's Chad Bradly's profile.
  if (/\/character\/id\/12781922/.test(location.pathname)) {
    // GIGACHAD
    $("#character-portrait-image").attr("src", "https://c.tenor.com/epNMHGvRyHcAAAAd/gigachad-chad.gif");

    // Adds custom banner background to my page.
    $("#portrait-and-basics, #character-header-customize-action-box, #update-box").addClass("slightly-transparent-box");
    $("#character-portrait-box").css("background-image", "url(\"https://i.imgur.com/dbwqHIt.png\")").addClass("with-banner");
  }
})();