FFProgs

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

目前為 2022-04-12 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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