Automatically redirect linux.do topic pages and topic links to nested view.
Pada tanggal
// ==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();
})();