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