Ranked Streams V2 (Grid & Sync Fix - Bottom)

Adds synced streams natively in MCSR Ranked at the bottom, supports split-screen grid and relative instant seeking.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Ranked Streams V2 (Grid & Sync Fix - Bottom)
// @namespace   Violentmonkey Scripts
// @match       *://*.mcsrranked.com/*
// @grant       GM_addStyle
// @version     1.0
// @run-at      document-idle
// @description Adds synced streams natively in MCSR Ranked at the bottom, supports split-screen grid and relative instant seeking.
// @license     MIT
// ==/UserScript==

(function () {
  // Configuration
  const TWITCH_EMBED_URL = "https://player.twitch.tv/js/embed/v1.js";
  const CONTAINER_SELECTOR = "div.min-h-64.flex-1.overflow-scroll.pt-2";
  const LINK_SELECTOR =
    'a[href^="https://www.twitch.tv/videos"][target="_blank"]';
  const BUTTON_SELECTOR =
    "button.cursor-pointer.border.px-2.py-1\\.5, div.cursor-pointer.border.px-2.py-1\\.5";

  // Declare basic variables
  let activePlayers = {}; // Map: videoId -> Twitch.Player instance
  let videoStartTimes = {}; // Map: videoId -> int (Earliest timestamp found for this video)
  let debounceTimer = null;

  // Add CSS for the streams grid layout
  // Bad styling
  const style = document.createElement("style");
  style.textContent = `
    #twitch-grid-container {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
      gap: 8px;
      width: 100%;
    }
    .twitch-stream-wrapper {
      position: relative;
      width: 100%;
      padding-top: 43.75%; /* 16:7 Aspect Ratio */
      background: #000;
      border-radius: 8px;
      overflow: hidden;
    }
    .twitch-stream-wrapper iframe {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
  `;
  document.head.appendChild(style);

  // --- Helpers ---
  // Parse the twitch url
  function parseTwitchUrl(url) {
    const match = /videos\/(\d+)(?:\?t=(\d+)s)?/.exec(url);
    if (match) {
      return {
        videoId: match[1],
        time: match[2] ? parseInt(match[2]) : 0,
      };
    }
    return null;
  }

  // Load the script required for playback from twitch
  function loadTwitchAPI(callback) {
    const script = document.createElement("script");
    script.id = "twitch-api-script";
    script.src = TWITCH_EMBED_URL;
    script.onload = callback;
    document.body.appendChild(script);
  }

  // --- Core Logic ---

  function initStreams() {
    const container = document.querySelector(CONTAINER_SELECTOR);
    const links = document.querySelectorAll(LINK_SELECTOR);

    if (!container || links.length === 0) return;

    // Prevent double injection
    if (document.getElementById("twitch-grid-container")) return;

    console.log("[Ranked Streams] Match detected. Initializing...");

    // 1. Identify Start Times (The "Zero Point" for each stream)
    videoStartTimes = {};
    const uniqueVideos = new Set();

    links.forEach((link) => {
      const data = parseTwitchUrl(link.href);
      if (data) {
        uniqueVideos.add(data.videoId);
        // We assume the earliest timestamp found for a video ID is the "Match Start"
        if (
          typeof videoStartTimes[data.videoId] === "undefined" ||
          data.time < videoStartTimes[data.videoId]
        ) {
          videoStartTimes[data.videoId] = data.time;
        }
      }
    });

    // 2. Setup / Add Grid Container
    const gridDiv = document.createElement("div");
    gridDiv.id = "twitch-grid-container";
    container.appendChild(gridDiv);

    activePlayers = {}; // Reset Twitch Players

    // 3. Create Players

    uniqueVideos.forEach((videoId) => {
      const startTime = videoStartTimes[videoId] || 0;
      const wrapper = document.createElement("div");
      wrapper.className = "twitch-stream-wrapper";
      const playerDivId = `twitch-embed-${videoId}`;
      const playerDiv = document.createElement("div");
      playerDiv.id = playerDivId;
      wrapper.appendChild(playerDiv);
      gridDiv.appendChild(wrapper);

      const player = new Twitch.Player(playerDivId, {
        video: videoId,
        time: `${startTime}s`,
        autoplay: true,
        muted: true,
        controls: true,
        width: "100%",
        controls: false,
        height: "100%",
        parent: ["mcsrranked.com"],
      });

      activePlayers[videoId] = player;
    });

    // 4. Intercept Timeline Clicks
    addButtonEventListeners();
    addLinkEventListeners();
  }

  // 5. Start observing for changes to grid
  function startObserver() {
    const observer = new MutationObserver(() => {
      const container = document.querySelector(CONTAINER_SELECTOR);
      const grid = document.getElementById("twitch-grid-container");

      if (container && !grid) {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(initStreams, 500);
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });

    setTimeout(initStreams, 1000);
  }

  // 6. Listen for changes in splits and detail level and reload streams.
  function addButtonEventListeners() {
    const buttons = document.querySelectorAll(BUTTON_SELECTOR);
    console.log(buttons);
    buttons.forEach((button) => {
      button.addEventListener("click", addLinkEventListeners);
    });
  }

  // Add link click listeners which take you that point in stream
  function addLinkEventListeners() {
    const links = document.querySelectorAll(LINK_SELECTOR);
    links.forEach((link) => {
      const newLink = link.cloneNode(true);
      link.parentNode.replaceChild(newLink, link);

      newLink.addEventListener("click", (e) => {
        e.preventDefault();
        e.stopPropagation();

        const data = parseTwitchUrl(newLink.href);
        if (!data) return;

        // Calculate Offset
        const originBaseTime = videoStartTimes[data.videoId];
        if (originBaseTime === undefined) return;

        const matchOffset = data.time - originBaseTime;
        console.log(`[Ranked] Syncing to Match Time +${matchOffset}s`);

        // Apply Offset to all players
        Object.keys(activePlayers).forEach((targetVideoId) => {
          const targetBaseTime = videoStartTimes[targetVideoId];
          // 1 second so it starts correctly
          const targetSeekTime = targetBaseTime + matchOffset - 1;
          activePlayers[targetVideoId].seek(Math.max(0, targetSeekTime));
        });
      });
    });
  }

  // Start script
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () =>
      loadTwitchAPI(startObserver),
    );
  } else {
    loadTwitchAPI(startObserver);
  }
})();