FFProgs

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

当前为 2022-04-12 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name FFProgs
  3. // @name:en FFProgs
  4. // @name:ja FFプログレス
  5. // @namespace k_fizzel
  6. // @version 1.0.0
  7. // @author k_fizzel
  8. // @description Adds MultiStream Vod Watching and a Button to view logs in xivanalysis also some minor improvements.
  9. // @description:en Adds MultiStream VOD Watching and a Button to view logs in xivanalysis also some minor improvements.
  10. // @description:ja マルチストリームVODウォッチングとxivanalysisでログを表示するためのボタンもいくつかのマイナーな改善を追加します。
  11. // @website https://www.fflogs.com/character/id/12781922
  12. // @icon https://assets.rpglogs.com/img/ff/favicon.png?v=2
  13. // @match https://www.fflogs.com/*
  14. // @match https://*.fflogs.com/*
  15. // @grant GM_addStyle
  16. // @grant unsafeWindow
  17. // @license MIT License
  18. // ==/UserScript==
  19.  
  20. /*
  21. To Do:
  22. refactor code to make it look better pick a standard and use es6 functions and jquery
  23. right click a log in the encounters page to highlight with a specific color it so that it stands out
  24.  
  25. pause video when it gets to the end of a replay
  26. have global event listeners instead of a bunch of click ones
  27. make the video player stay in the same x y on log pull change
  28. Export current vod data so that you can import it
  29. when you share a twitch/youtube url make the timestamp the start
  30. make offset work with decimals
  31. Make youtube player work and private youtube streams as well
  32. github once I get to 1.0.0 with a readme on how to install delete this current git repo
  33.  
  34. Maybe:
  35. add keybindings
  36. make the player keep the same aspect ratio
  37. make player start at a larger size
  38. greasy fork pull from github for updates
  39.  
  40.  
  41. */
  42. (function () {
  43.  
  44. // Helper functions
  45. function addGlobalEventListener(type, selector, callaBack) {
  46. document.addEventListener(type, e => {
  47. if (e.target.matches(selector)) callaBack(e);
  48. })
  49. }
  50.  
  51. // Adblock.
  52. $("#top-banner, #bottom-banner, #playwire-video-container, #patron-box, #gear-box-ad").remove();
  53.  
  54. // Remove alt-text from item images. (Alt text looks awful when api is not up to date and relics )
  55. $(".table-icon").removeAttr("alt");
  56.  
  57. // Adds xivanalysis button.
  58. const updateXIVAnalysisUrl = () => { $("#xivanalysis-tab").attr("href", `https://xivanalysis.com/report-redirect/${location.href}`); }
  59. $("#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>`)
  60. $("#xivanalysis-tab").click(updateXIVAnalysisUrl);
  61.  
  62. // Checks if video player button exists
  63. const videoButton = document.querySelector(".replay-video");
  64. if (/\/reports\/.+/.test(location.pathname)) {
  65. const streams = {
  66. // test data
  67. "Chad_Bradly": { platform: 1, url: 'https://www.twitch.tv/videos/787291919?t=01h04m19s', offset: -1312 },
  68. "Charlie_Cerise": { platform: 2, url: 'https://www.youtube.com/watch?v=d_BBgHPIlWg' },
  69. "Gale_Eternia": { platform: 2, url: 'https://www.youtube.com/watch?v=d_BBgHPIlWg' },
  70. "Glyphimor_Epsilon": { platform: 1, url: 'https://www.twitch.tv/videos/787291919?t=01h04m19s' },
  71. "Kara_Doomfist": { platform: 2, url: 'https://www.youtube.com/watch?v=d_BBgHPIlWg' },
  72. "M'aique_Delieur": { platform: 1, url: 'https://www.twitch.tv/videos/787291919?t=01h04m19s' },
  73. "Nishi_Michu": { platform: 2, url: 'https://www.youtube.com/watch?v=d_BBgHPIlWg' },
  74. "Pual_Pual": { platform: 1, url: 'https://www.twitch.tv/videos/787291919?t=01h04m19s' }
  75. }
  76. let person, videoFrame, videoPlayer, allowPlay = false, timesUpdate = 0, videPlayerOpen = false, multiStreamOpen = false, multiStreamMovement = false, isDragging = false;
  77.  
  78. // iframe api's
  79. const firstScriptTag = $("script")[0]
  80. $(`<script src="https://player.twitch.tv/js/embed/v1.js"></script>
  81. <script src="https://www.youtube.com/iframe_api"></script>`).insertBefore(firstScriptTag);
  82.  
  83. // doesn't work with jquery LMAO
  84. const interactModule = document.createElement("script");
  85. interactModule.type = "module";
  86. interactModule.innerHTML = `
  87. import interact from "https://cdn.interactjs.io/v1.10.11/interactjs/index.js";
  88. interact("#video-frame").resizable({
  89. edges: { left: true, right: true, bottom: true, top: true },
  90. listeners: {
  91. move (event) {
  92. let target = event.target;
  93. let x = (parseFloat(target.getAttribute("data-x")) || 0);
  94. let y = (parseFloat(target.getAttribute("data-y")) || 0);
  95.  
  96. target.style.width = event.rect.width + "px";
  97. target.style.height = event.rect.height + "px";
  98.  
  99. x += event.deltaRect.left;
  100. y += event.deltaRect.top;
  101.  
  102. target.style.transform = \`translate(\${x}px, \${y}px)\`;
  103.  
  104. target.setAttribute("data-x", x);
  105. target.setAttribute("data-y", y);
  106. }
  107. },
  108. inertia: true,
  109. modifiers: [
  110. interact.modifiers.aspectRatio({
  111. // make sure the width is always double the height
  112. ratio: 1.72,
  113.  
  114. modifiers: [
  115. interact.modifiers.restrictRect({ endOnly: true }),
  116. ],
  117. }),
  118. interact.modifiers.restrictSize({
  119. min: { width: 560, height: 337 }
  120. })
  121. ]
  122. })
  123. .draggable({
  124. listeners: {
  125. move (event) {
  126. let target = event.target;
  127. let x = (parseFloat(target.getAttribute("data-x")) || 0) + event.dx;
  128. let y = (parseFloat(target.getAttribute("data-y")) || 0) + event.dy;
  129.  
  130. target.style.transform = \`translate(\${x}px, \${y}px)\`;
  131.  
  132. target.setAttribute("data-x", x);
  133. target.setAttribute("data-y", y);
  134. },
  135. },
  136. inertia: true,
  137. modifiers: [
  138. interact.modifiers.restrictRect({
  139. restriction: "body",
  140. endOnly: true
  141. })
  142. ]
  143. })`;
  144. firstScriptTag.parentNode.insertBefore(interactModule, firstScriptTag)
  145.  
  146. // updates the position of the video to match the log
  147. const updatePosition = () => {
  148. const timeSinceFirstPull = parseInt(streams[person].offset) + ((unsafeWindow.replayPosition - unsafeWindow.fights[0].start_time) / 1000);
  149. videoPlayer.seek(timeSinceFirstPull)
  150. }
  151.  
  152. // returns if the player is allows to play
  153. const togglePlay = () => {
  154. allowPlay = !allowPlay;
  155. return !allowPlay;
  156. }
  157.  
  158. // make the log play button play the video
  159. $("#play-button").attr("onclick", "toggleReplayState(this)").click((e) => {
  160. if (videoPlayer.isPaused()) {
  161. videoPlayer.play();
  162. togglePlay();
  163. } else {
  164. videoPlayer.pause();
  165. togglePlay();
  166. }
  167. });
  168. // updates video when moving via the replay bar
  169.  
  170. $("#graph").on("mousedown", (e) => {
  171. if (e.type == "mousedown") {
  172. isDragging = true;
  173. updatePosition();
  174. }
  175. });
  176. $("body").on("mousemove mouseup", (e) => {
  177. if (videPlayerOpen && multiStreamOpen) {
  178. if (e.type == "mouseup") {
  179. if (isDragging) {
  180. isDragging = false;
  181. updatePosition();
  182. }
  183. }
  184. }
  185. })
  186.  
  187. function stopLoading() {
  188. const loading = document.getElementById("multistream-loading-icon")
  189. if (loading) {
  190. loading.outerHTML = "";
  191. }
  192.  
  193. const multistreamPlayer = document.getElementById("multistream_player");
  194. multistreamPlayer.style.display = "block";
  195. }
  196.  
  197. function onTwitchPlayerReady() {
  198. updatePosition();
  199. videoPlayer.play()
  200. stopLoading();
  201. }
  202.  
  203. function onTwitchPlayerEnded() {
  204.  
  205. }
  206. function onTwitchPlayerPlaying() {
  207. if (!allowPlay) videoPlayer.pause()
  208.  
  209. }
  210. function onTwitchPlayerPause() {
  211. if (allowPlay) videoPlayer.play()
  212. }
  213. function onTwitchPlayerSeek() {
  214.  
  215. }
  216.  
  217. function onYoutubePlayerReady(e) {
  218. stopLoading();
  219.  
  220. }
  221.  
  222. function onYoutubePlayerStateChange(e) {
  223. switch (e) {
  224. case 0: //onYouTubePlayerEnded
  225.  
  226. break;
  227. case 1: //onYouTubePlayerPlaying
  228.  
  229. break;
  230. case 2: //onYouTubePlayerPaused
  231.  
  232. break;
  233. case 3: //onYouTubePlayerBuffering
  234.  
  235. break;
  236. case 5: //onYouTubePlayerVideoCued
  237.  
  238. break;
  239.  
  240. default:
  241. break;
  242. }
  243. }
  244.  
  245. function showMultiStreamPlayer() {
  246. if ($("#video-frame").css("display") === "block") {
  247. videoFrame = document.getElementById("video-frame-inner");
  248. 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>";
  249. let playerDiv = document.createElement("div");
  250. playerDiv.id = "multistream_player";
  251. videoFrame.innerHTML += playerDiv.outerHTML;
  252. playerDiv = document.getElementById("multistream_player");
  253. playerDiv.style = "width: 100%; height: 100%; display: none;";
  254. // If noddy is selected play fist stream;
  255.  
  256. const stream = streams[person];
  257. const videoID = new URL(stream.url)
  258. if (stream.platform === 1) {
  259. videoPlayer = new Twitch.Player("multistream_player", {
  260. width: "100%",
  261. height: "100%",
  262. autoplay: false,
  263. muted: true,
  264. video: videoID.pathname.split("/")[2],
  265. });
  266. videoPlayer.addEventListener(Twitch.Player.READY, onTwitchPlayerReady);
  267.  
  268. videoPlayer.addEventListener(Twitch.Player.ENDED, onTwitchPlayerEnded);
  269. videoPlayer.addEventListener(Twitch.Player.PLAYING, onTwitchPlayerPlaying);
  270. videoPlayer.addEventListener(Twitch.Player.PAUSE, onTwitchPlayerPause);
  271. videoPlayer.addEventListener(Twitch.Player.SEEK, onTwitchPlayerSeek);
  272. }
  273.  
  274.  
  275. if (stream.platform === 2) {
  276. videoPlayer = new YT.Player("multistream_player", {
  277. height: "100%",
  278. width: "100%",
  279. videoId: videoID.searchParams.get("v"),
  280. playerVars: {
  281. controls: 0
  282. }
  283. })
  284. videoPlayer.addEventListener("onReady", onYoutubePlayerReady);
  285. videoPlayer.addEventListener("onStateChange", onYoutubePlayerStateChange);
  286. }
  287. }
  288.  
  289. }
  290.  
  291. function toggleMultiStreamPlayer() {
  292. multiStreamOpen = !multiStreamOpen
  293.  
  294. updateMultistreamButton()
  295. if (multiStreamOpen) {
  296. showMultiStreamPlayer()
  297. }
  298. }
  299.  
  300. function updateMultistreamButton() {
  301. const multiStreamViewButton = document.getElementById("multistream-view")
  302. if (multiStreamOpen) {
  303. multiStreamViewButton.style.backgroundColor = "#000060"
  304. } else {
  305. multiStreamViewButton.style.backgroundColor = ""
  306. }
  307. }
  308.  
  309. function showMultistreamOptions() {
  310. if (multiStreamOpen) {
  311. toggleMultiStreamPlayer()
  312. }
  313. videoFrame = document.getElementById("video-frame-inner");
  314. addGlobalEventListener("input", ".url-table-row", (e) => {
  315. const [user, action] = e.target.id.split("-");
  316. const value = e.target.value;
  317. if (action === "stream_url") {
  318. let url, platform;
  319. let err = false;
  320. try {
  321. url = new URL(value);
  322. if (url.hostname === "www.twitch.tv" && url.pathname.match(/\/videos\/\d+/g)) {
  323. platform = 1;
  324. } else if (url.hostname === "www.youtube.com" && url.pathname === "/watch" && url.searchParams.get("v")) {
  325. platform = 2;
  326. } else {
  327. throw "not a valid platform"
  328. }
  329. }
  330. catch (error) {
  331. if (streams[user]) {
  332. delete streams[user];
  333. }
  334. if (error) err = true;
  335. }
  336. finally {
  337. if (!err) {
  338. if (!streams[user]) streams[user] = {};
  339. streams[user].platform = platform;
  340. streams[user].url = url.href;
  341. }
  342. }
  343. }
  344. if (action === "stream_offset") {
  345. if (streams[user] && streams[user].url) {
  346. let offset = parseInt(value);
  347. streams[user].offset = offset;
  348. }
  349. }
  350. })
  351.  
  352. const div = document.createElement("div");
  353. div.style = "text-align: center;";
  354.  
  355. const form = document.createElement("form");
  356.  
  357. form.style = "margin: 0;";
  358. form.acceptCharset = "utf-8";
  359. form.method = "GET";
  360. form.action = "javascript:void(0);";
  361.  
  362. const table = document.createElement("table");
  363. table.style = "border-collapse: separate; border-spacing: 8px; margin: auto; text-align: left;";
  364.  
  365. //Table Information
  366. const infoRow = document.createElement("tr");
  367. const nameInfo = document.createElement("td");
  368. const URLInfo = document.createElement("td");
  369. const offsetInfo = document.createElement("td");
  370. nameInfo.innerHTML = "Name:";
  371. URLInfo.innerHTML = "Stream URL:";
  372. offsetInfo.innerHTML = "Offset:";
  373. offsetInfo.style = "max-width: 60px;";
  374.  
  375. infoRow.append(nameInfo, URLInfo, offsetInfo);
  376. table.appendChild(infoRow);
  377.  
  378. const raidFrames = document.querySelectorAll(".raid-frame-contents");
  379. const raiders = [];
  380. raidFrames.forEach((frame) => {
  381. raiders.push(frame.innerHTML);
  382. })
  383.  
  384. raiders.forEach((person) => {
  385. person = person.replace(" ", "_");
  386.  
  387. const tableRow = document.createElement("tr");
  388. const nameData = document.createElement("td");
  389. nameData.innerHTML = person;
  390. tableRow.appendChild(nameData);
  391. const streamData = document.createElement("td");
  392. const streamUrl = document.createElement("input");
  393. streamUrl.style = "min-width: 260px;"
  394. streamUrl.type = "url";
  395. streamUrl.id = `${person}-stream_url`;
  396. streamUrl.name = `${person}-stream_url`;
  397. streamUrl.placeholder = "youtube.com/watch?v= or twitch.tv/videos/";
  398. if (streams[person] && streams[person].url) {
  399. streamUrl.setAttribute("value", streams[person].url);
  400. }
  401. streamUrl.classList.add("url-table-row");
  402. streamData.appendChild(streamUrl);
  403. tableRow.appendChild(streamData);
  404.  
  405. const offsetData = document.createElement("td");
  406. const offsetTime = document.createElement("input");
  407. offsetTime.type = "number";
  408. offsetTime.style = "width: 5em;";
  409. offsetTime.id = `${person}-stream_offset`;
  410. offsetTime.name = `${person}-stream_offset`;
  411. offsetTime.placeholder = "# in sec";
  412. if (streams[person] && streams[person].offset) {
  413. offsetTime.setAttribute("value", streams[person].offset)
  414. }
  415. offsetTime.classList.add("url-table-row");
  416.  
  417. offsetData.appendChild(offsetTime);
  418. tableRow.appendChild(offsetData);
  419.  
  420. table.appendChild(tableRow);
  421. })
  422.  
  423. form.appendChild(table);
  424. div.appendChild(form);
  425. videoFrame.innerHTML = div.outerHTML;
  426. }
  427.  
  428. function toggleMultiStreamMove() {
  429. multiStreamMovement = !multiStreamMovement;
  430. const frameInner = document.getElementById("video-frame-inner");
  431. const resizeButton = document.getElementById("multistream-resize");
  432. if (multiStreamMovement) {
  433. resizeButton.style.backgroundColor = "#000060"
  434. frameInner.style.pointerEvents = "none"
  435. } else {
  436. resizeButton.style.backgroundColor = ""
  437. frameInner.style.pointerEvents = ""
  438. }
  439. }
  440. let prvPerson;
  441. document.addEventListener("click", (e) => {
  442. let selected = document.querySelector(".raid-frame.selected .raid-frame-contents");
  443. (selected) ? person = selected.innerHTML.replace(" ", "_") : person = Object.keys(streams)[0];
  444. if (prvPerson !== person) {
  445. if (multiStreamOpen) {
  446. showMultiStreamPlayer();
  447. }
  448. }
  449. prvPerson = person;
  450. })
  451.  
  452. const positionGraph = document.getElementById("position-graph");
  453. const observer = new MutationObserver(function () {
  454. timesUpdate++;
  455.  
  456. if (timesUpdate > 2) {
  457. if (!positionGraph.style.opacity) {
  458. const videoButton = document.querySelector(".replay-video");
  459. if (videPlayerOpen) {
  460. allowPlay = false;
  461. videoButton.click()
  462. videoButton.click()
  463. updateMultistreamButton()
  464. toggleMultiStreamMove()
  465. toggleMultiStreamMove()
  466. }
  467. }
  468. }
  469. });
  470. observer.observe(positionGraph, { attributes: true });
  471.  
  472. videoButton.addEventListener("click", (e) => {
  473. $("#fullscreen-video, #remove-video, #select-video, #mute-video").remove();
  474.  
  475. const frame = document.getElementById("video-frame");
  476. frame.style.touchAction = "none";
  477. frame.style.boxSizing = "border-box"
  478.  
  479.  
  480. videoFrame = document.getElementById("video-frame-inner");
  481. videPlayerOpen = !videPlayerOpen
  482. if (multiStreamOpen) {
  483. setTimeout(showMultiStreamPlayer, 500)
  484. }
  485. // Creates Menu Below Video Player
  486. const multiStreamOptions = document.getElementById("multistream-options");
  487. if (!multiStreamOptions) {
  488.  
  489. const videoFrameControls = document.getElementById("video-frame-controls");
  490. videoFrameControls.style.cursor = "ns-resize"
  491. const multiStreamO = document.createElement("span");
  492. multiStreamO.style = "float: right; cursor: pointer;";
  493. multiStreamO.id = "multistream-options";
  494. multiStreamO.classList.add("graph-legend-button");
  495. multiStreamO.onclick = showMultistreamOptions;
  496. multiStreamO.innerText = "Multistream options"
  497. videoFrameControls.appendChild(multiStreamO);
  498.  
  499. const multiStreamV = document.createElement("span");
  500. multiStreamV.style = "margin-right: -1px; float: right; cursor: pointer;";
  501. multiStreamV.id = "multistream-view";
  502. multiStreamV.classList.add("graph-legend-button");
  503. multiStreamV.onclick = toggleMultiStreamPlayer;
  504. multiStreamV.innerText = "Multistream View";
  505. videoFrameControls.appendChild(multiStreamV);
  506.  
  507. const resizeButton = document.createElement("span");
  508. resizeButton.style = "float: left; cursor: pointer;";
  509. resizeButton.id = "multistream-resize";
  510. resizeButton.classList.add("graph-legend-button");
  511. resizeButton.onclick = toggleMultiStreamMove;
  512. resizeButton.innerText = "Resize/Move";
  513. videoFrameControls.appendChild(resizeButton);
  514. }
  515. })
  516. }
  517.  
  518. // Checks if it's Chad Bradly's profile.
  519. if (/\/character\/id\/12781922/.test(location.pathname)) {
  520. // GIGACHAD
  521. $("#character-portrait-image").attr("src", "https://c.tenor.com/epNMHGvRyHcAAAAd/gigachad-chad.gif");
  522.  
  523. // Adds custom banner background to my page.
  524. $("#portrait-and-basics, #character-header-customize-action-box, #update-box").addClass("slightly-transparent-box");
  525. $("#character-portrait-box").css("background-image", "url(\"https://i.imgur.com/dbwqHIt.png\")").addClass("with-banner");
  526. }
  527. })();