您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds MultiStream Vod Watching and a Button to view logs in xivanalysis also some minor improvements.
当前为
// ==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"); } })();