SimpCity - Open Unread Alerts in Tabs

Adds a button next to "Alerts" to open all unread alerts in new tabs with a delay to prevent rate limiting.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         SimpCity - Open Unread Alerts in Tabs
// @namespace    http://tampermonkey.net/
// @version      0.2.1
// @description  Adds a button next to "Alerts" to open all unread alerts in new tabs with a delay to prevent rate limiting.
// @author       bitter.beer
// @match        https://simpcity.cr/*
// @match        https://simpcity.is/*
// @match        https://simpcity.cz/*
// @match        https://simpcity.hk/*
// @match        https://simpcity.rs/*
// @match        https://simpcity.ax/*
// @grant        GM_openInTab
// @connect      simpcity.cr
// @connect      simpcity.is
// @connect      simpcity.cz
// @connect      simpcity.hk
// @connect      simpcity.rs
// @connect      simpcity.ax
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";

  // ===== CONFIG =====
  // Delay (in ms) between opening each unread alert
  const OPEN_DELAY_MS = 3000; // 1 second — adjust as needed

  // Max number of unread alerts to open per click (set to null for all)
  const MAX_TO_OPEN = null; // e.g. 10 to limit to 10 alerts

  // ===== CORE LOGIC =====

  // Adaptive opening queue state (must be defined before usage)
  let isProcessingQueue = false;
  let waitingForNext = false;
  let currentQueueTimer = null;

  /**
   * Find all unread alert links (the actual thread/post links).
   * These are the `.fauxBlockLink-blockLink` anchors inside `.alert.is-unread` items.
   */
  function getUnreadAlertLinks() {
    const selector =
      ".js-alertsMenuBody li.alert.is-unread .fauxBlockLink-blockLink";
    const links = Array.from(document.querySelectorAll(selector));
    return links;
  }

  /**
   * Opens all unread alert links in new tabs with a delay between each.
   */
  function openUnreadAlerts() {
    const links = getUnreadAlertLinks();

    if (!links.length) {
      alert("No unread alerts found.");
      return;
    }

    const totalToOpen = MAX_TO_OPEN
      ? Math.min(MAX_TO_OPEN, links.length)
      : links.length;

    let index = 0;

    // Initialize queue state
    isProcessingQueue = true;
    waitingForNext = false;
    if (currentQueueTimer) {
      clearTimeout(currentQueueTimer);
      currentQueueTimer = null;
    }

    function openNext() {
      // Finished queue
      if (index >= totalToOpen) {
        isProcessingQueue = false;
        waitingForNext = false;
        currentQueueTimer = null;
        return;
      }

      const link = links[index++];
      if (!link || !link.href) {
        openNext();
        return;
      }

      const url = link.href;

      try {
        if (typeof GM_openInTab === "function") {
          GM_openInTab(url, { active: false, insert: true, setParent: true });
        } else {
          window.open(url, "_blank");
        }
      } catch (e) {
        console.error("Failed to open alert tab:", e);
      }

      // Prepare for next opening (either early via message or fallback timer)
      if (index < totalToOpen) {
        waitingForNext = true;
        currentQueueTimer = setTimeout(() => {
          // Fallback path if tab load is slower than delay
          if (!waitingForNext) return; // Already handled by early open
          waitingForNext = false;
          openNext();
        }, OPEN_DELAY_MS);
      } else {
        // Queue will finish after last open
        isProcessingQueue = false;
      }
    }

    // Expose openNext so message handler can trigger early advance
    window.__scOpenNext = openNext;

    openNext();
  }

  /**
   * Ensure the "Open Unread" button exists only when there are unread alerts.
   * Creates the button if needed, removes it if none remain.
   */
  function ensureUnreadButtonForAlertsHeader(headerEl) {
    if (!headerEl) return;
    const existingBtn = headerEl.querySelector("button[data-unread-open-btn]");
    const unreadCount = getUnreadAlertLinks().length;

    if (unreadCount === 0) {
      if (existingBtn) existingBtn.remove();
      return;
    }

    if (existingBtn) return; // Already present and unread exist

    const btn = document.createElement("button");
    btn.type = "button";
    btn.textContent = "Open Unread";
    btn.title = "Open all unread alerts in new tabs (with delay)";
    btn.setAttribute("data-unread-open-btn", "1");
    btn.style.marginLeft = "0.5em";
    btn.style.fontSize = "0.9em";
    btn.style.cursor = "pointer";
    btn.style.padding = "2px 6px";
    btn.style.borderRadius = "3px";
    btn.style.border = "1px solid rgba(255,255,255,0.2)";
    btn.style.background = "hsla(187, 73%, 52%, 0.5)";

    btn.addEventListener("click", function (e) {
      e.stopPropagation();
      openUnreadAlerts();
    });

    headerEl.appendChild(btn);
  }

  /**
   * Find the Alerts header(s) and ensure button visibility accordingly.
   */
  function updateAlertsHeaders(root = document) {
    const headers = root.querySelectorAll("h3.menu-header");
    headers.forEach((h3) => {
      if (h3.textContent.trim() === "Alerts") {
        ensureUnreadButtonForAlertsHeader(h3);
      }
    });
  }

  // Try immediately (in case the menu is already in DOM)
  updateAlertsHeaders(document);

  // ===== MutationObserver to handle dynamically inserted alerts menu =====
  const observer = new MutationObserver((mutationList) => {
    for (const mutation of mutationList) {
      mutation.addedNodes.forEach((node) => {
        if (!(node instanceof HTMLElement)) return;

        // If a chunk of menu content is added, search within it
        if (
          node.matches(".js-alertsMenuBody, .menu, .menu-content") ||
          node.querySelector(".js-alertsMenuBody")
        ) {
          updateAlertsHeaders(node);
        }

        // General fallback: if any h3.menu-header is added anywhere
        if (
          node.matches("h3.menu-header") ||
          node.querySelector("h3.menu-header")
        ) {
          updateAlertsHeaders(node);
        }
      });
    }
  });

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

  // Periodically re-check in case read state changes without DOM additions (optional safeguard)
  setInterval(() => updateAlertsHeaders(document), 1000);

  // ===== Adaptive Opening Support =====

  // Listen for load completion messages from child tabs
  window.addEventListener("message", (e) => {
    // Basic validation: expecting our custom type
    if (!e.data || e.data.type !== "SC_TAB_LOADED") return;
    if (!isProcessingQueue || !waitingForNext) return;
    // Early proceed
    waitingForNext = false;
    if (currentQueueTimer) {
      clearTimeout(currentQueueTimer);
      currentQueueTimer = null;
    }
    // Continue queue immediately
    // Slight micro-delay to allow browser idle time
    // Proceed immediately (micro-delay not strictly needed)
    if (typeof window.__scOpenNext === "function") {
      window.__scOpenNext();
    }
  });
  // Ensure global callback reference exists even before first queue
  if (typeof window.__scOpenNext !== "function") {
    window.__scOpenNext = function () {};
  }

  // In newly opened tabs, notify opener early once load finishes
  if (window.opener) {
    window.addEventListener("load", () => {
      try {
        window.opener.postMessage({ type: "SC_TAB_LOADED" }, "*");
      } catch (err) {
        // Ignore
      }
    });
  }
})();