LINUX DO Auto Nested View

Automatically redirect linux.do topic pages and topic links to nested view.

Verzia zo dňa 04.06.2026. Pozri najnovšiu verziu.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         LINUX DO Auto Nested View
// @name:zh-CN   LINUX DO 自动嵌套视图
// @namespace    https://github.com/kai-wei-kfuse/linux-do-auto-nested-view
// @version      1.5.1
// @description  Automatically redirect linux.do topic pages and topic links to nested view.
// @description:zh-CN 自动将 linux.do 主题页和主题链接切换到嵌套视图。
// @author       kai-wei-kfuse
// @license      MIT
// @match        https://linux.do/*
// @homepageURL  https://github.com/kai-wei-kfuse/linux-do-auto-nested-view
// @supportURL   https://github.com/kai-wei-kfuse/linux-do-auto-nested-view/issues
// @run-at       document-start
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// ==/UserScript==

(function () {
  "use strict";

  const ENABLED_STORAGE_KEY = "linuxdo-auto-nested-view-enabled";
  const pageWindow =
    typeof unsafeWindow === "undefined" ? window : unsafeWindow;

  let lastHandledUrl = "";
  let observer = null;
  let menuCommandId = null;
  let autoConvertEnabled = loadAutoConvertEnabled();
  const rewrittenLinks = new WeakSet();

  function loadAutoConvertEnabled() {
    try {
      return GM_getValue(ENABLED_STORAGE_KEY, true) !== false;
    } catch {
      return true;
    }
  }

  function saveAutoConvertEnabled(enabled) {
    autoConvertEnabled = enabled;

    try {
      GM_setValue(ENABLED_STORAGE_KEY, enabled);
    } catch {
      // Ignore storage failures so the script still works with its default state.
    }
  }

  function registerToggleMenu() {
    if (typeof GM_registerMenuCommand !== "function") {
      return;
    }

    if (
      menuCommandId !== null &&
      typeof GM_unregisterMenuCommand === "function"
    ) {
      GM_unregisterMenuCommand(menuCommandId);
    }

    const status = autoConvertEnabled ? "开启" : "关闭";
    menuCommandId = GM_registerMenuCommand(
      `自动转换嵌套视图:${status}`,
      () => {
        saveAutoConvertEnabled(!autoConvertEnabled);
        registerToggleMenu();
        applyAutoConvertState();

        if (!autoConvertEnabled) {
          pageWindow.location.reload();
        }
      }
    );
  }

  function stopObserver() {
    if (!observer) {
      return;
    }

    observer.disconnect();
    observer = null;
  }

  function applyAutoConvertState() {
    lastHandledUrl = "";

    if (!autoConvertEnabled) {
      stopObserver();
      return;
    }

    startObserver();
    rewriteTopicLinks();
    redirectToNested();
  }

  function extractTopicId(pathname) {
    const nestedMatch = pathname.match(/^\/n\/topic\/(\d+)(?:\/.*)?$/);
    if (nestedMatch) {
      return nestedMatch[1];
    }

    const topicMatch = pathname.match(/^\/t\/[^/]+\/(\d+)(?:\/.*)?$/);
    if (topicMatch) {
      return topicMatch[1];
    }

    return null;
  }

  function isTopicPage(pathname) {
    return pathname.startsWith("/t/") && Boolean(extractTopicId(pathname));
  }

  function isNestedPage(pathname) {
    return pathname.startsWith("/n/topic/") && Boolean(extractTopicId(pathname));
  }

  function toNestedUrl(urlLike) {
    const url = new URL(urlLike, pageWindow.location.origin);
    const topicId = extractTopicId(url.pathname);
    if (!topicId) {
      return null;
    }

    url.pathname = `/n/topic/${topicId}`;
    return url.toString();
  }

  function rewriteTopicLinks(root = document) {
    if (!autoConvertEnabled) {
      return;
    }

    const links = root.querySelectorAll('a[href]');
    for (const link of links) {
      const nestedUrl = toNestedUrl(link.href);
      if (!nestedUrl) {
        continue;
      }

      if (link.href === nestedUrl && rewrittenLinks.has(link)) {
        continue;
      }

      link.href = nestedUrl;
      rewrittenLinks.add(link);
    }
  }

  function rewriteLinkElement(link) {
    if (!autoConvertEnabled) {
      return;
    }

    if (!link || !link.href) {
      return;
    }

    const nestedUrl = toNestedUrl(link.href);
    if (!nestedUrl || link.href === nestedUrl) {
      return;
    }

    link.href = nestedUrl;
    rewrittenLinks.add(link);
  }

  function findAnchorFromEventTarget(target) {
    if (!(target instanceof Element)) {
      return null;
    }

    return target.closest("a[href]");
  }

  function isNotificationArea(target) {
    if (!(target instanceof Element)) {
      return false;
    }

    return Boolean(
      target.closest(
        [
          "#quick-access-notifications",
          ".user-notifications-list",
          ".notifications-list",
          ".notification-history",
          ".menu-panel.notifications",
          ".quick-access-panel",
          ".notification",
          "[data-notification-id]",
        ].join(",")
      )
    );
  }

  function forceNotificationNavigation(event) {
    if (!autoConvertEnabled) {
      return;
    }

    if (!isNotificationArea(event.target)) {
      return;
    }

    const link = findAnchorFromEventTarget(event.target);
    if (!link) {
      return;
    }

    const nestedUrl = toNestedUrl(link.href);
    if (!nestedUrl) {
      return;
    }

    event.preventDefault();
    event.stopPropagation();
    event.stopImmediatePropagation?.();
    pageWindow.location.assign(nestedUrl);
  }

  function interceptNavigationEvent(event) {
    if (!autoConvertEnabled) {
      return;
    }

    const link = findAnchorFromEventTarget(event.target);
    if (!link) {
      return;
    }

    rewriteLinkElement(link);
  }

  function redirectToNested() {
    if (!autoConvertEnabled) {
      lastHandledUrl = "";
      return;
    }

    const { pathname, href } = pageWindow.location;

    if (href === lastHandledUrl) {
      return;
    }

    if (isNestedPage(pathname)) {
      const normalizedNestedUrl = toNestedUrl(href);
      if (normalizedNestedUrl && normalizedNestedUrl !== href) {
        lastHandledUrl = href;
        pageWindow.location.replace(normalizedNestedUrl);
        return;
      }

      lastHandledUrl = href;
      return;
    }

    if (!isTopicPage(pathname)) {
      lastHandledUrl = href;
      return;
    }

    const link = document.querySelector("a.nested-view-link");
    if (link && link.href) {
      lastHandledUrl = href;
      pageWindow.location.href = link.href;
      return;
    }

    const nestedUrl = toNestedUrl(href);
    if (nestedUrl && nestedUrl !== href) {
      lastHandledUrl = href;
      pageWindow.location.href = nestedUrl;
    }
  }

  function startObserver() {
    if (!autoConvertEnabled) {
      stopObserver();
      return;
    }

    if (observer) {
      observer.disconnect();
    }

    observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType !== Node.ELEMENT_NODE) {
            continue;
          }

          if (node.matches?.("a[href]")) {
            rewriteTopicLinks(node.parentElement ?? document);
            continue;
          }

          rewriteTopicLinks(node);
        }
      }

      redirectToNested();
    });

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

  function hookHistoryMethod(methodName) {
    const original = pageWindow.history[methodName];
    pageWindow.history[methodName] = function (...args) {
      if (autoConvertEnabled && args.length >= 3 && args[2] != null) {
        const nestedUrl = toNestedUrl(args[2]);
        if (nestedUrl) {
          args[2] = nestedUrl;
        }
      }

      const result = original.apply(this, args);
      if (autoConvertEnabled) {
        setTimeout(redirectToNested, 0);
      }
      return result;
    };
  }

  registerToggleMenu();
  hookHistoryMethod("pushState");
  hookHistoryMethod("replaceState");
  window.addEventListener("popstate", () => setTimeout(redirectToNested, 0));
  window.addEventListener("DOMContentLoaded", () => {
    rewriteTopicLinks();
    redirectToNested();
  });
  window.addEventListener("click", forceNotificationNavigation, true);
  window.addEventListener("pointerdown", interceptNavigationEvent, true);
  window.addEventListener("mousedown", interceptNavigationEvent, true);
  window.addEventListener("click", interceptNavigationEvent, true);
  window.addEventListener("auxclick", interceptNavigationEvent, true);

  applyAutoConvertState();
})();