LINUX.SB Navigation Progress

为站内页面跳转显示轻量的顶部进度条,不拦截原生导航。

2026/08/21のページです。最新版はこちら

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

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         LINUX.SB Navigation Progress
// @namespace    https://linux.sb/
// @version      1.2.0
// @description  为站内页面跳转显示轻量的顶部进度条,不拦截原生导航。
// @match        https://linux.sb/*
// @grant        none
// @run-at       document-start
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    var SHOW_DELAY_MS = 100;
    var SLOW_PROGRESS_DELAY_MS = 2000;
    var SAFETY_TIMEOUT_MS = 15000;
    var COMPLETE_DURATION_MS = 180;
    var COMPLETE_HOLD_MS = 220;
    var FADE_DURATION_MS = 160;
    var MOUNT_RETRY_MS = 16;
    var HISTORY_FINISH_DELAY_MS = 80;
    var STORAGE_KEY = 'lsb-navigation-progress-pending-v1';
    var CONTAINER_ID = 'lsb-navigation-progress';
    var STYLE_ID = 'lsb-navigation-progress-style';
    var FILL_CLASS = 'lsb-navigation-progress-fill';

    var CSS_TEXT = [
        '#' + CONTAINER_ID + ' {',
        '  position: fixed;',
        '  top: 0;',
        '  left: 0;',
        '  z-index: 2147483647;',
        '  width: 100%;',
        '  height: 3px;',
        '  overflow: hidden;',
        '  box-sizing: border-box;',
        '  background: rgba(30, 30, 32, .16);',
        '  opacity: 0;',
        '  visibility: hidden;',
        '  pointer-events: none;',
        '  transition: opacity ' + FADE_DURATION_MS + 'ms ease, visibility 0s linear ' + FADE_DURATION_MS + 'ms;',
        '  contain: layout paint;',
        '}',
        '#' + CONTAINER_ID + '[data-visible="true"] {',
        '  opacity: 1;',
        '  visibility: visible;',
        '  transition-delay: 0s;',
        '}',
        '#' + CONTAINER_ID + ' .' + FILL_CLASS + ' {',
        '  width: 100%;',
        '  height: 100%;',
        '  background: #FEB005;',
        '  box-shadow: 0 0 8px rgba(254, 176, 5, .55);',
        '  transform: scaleX(0);',
        '  transform-origin: left center;',
        '  transition-property: transform;',
        '  transition-timing-function: ease-out;',
        '  will-change: transform;',
        '}',
        '@media (prefers-reduced-motion: reduce) {',
        '  #' + CONTAINER_ID + ',',
        '  #' + CONTAINER_ID + ' .' + FILL_CLASS + ' {',
        '    transition: none !important;',
        '  }',
        '}'
    ].join('\n');

    var container = null;
    var fill = null;
    var showTimer = 0;
    var slowTimer = 0;
    var safetyTimer = 0;
    var fadeTimer = 0;
    var resetTimer = 0;
    var mountTimer = 0;
    var completionTimer = 0;
    var pendingClickEvent = null;
    var activeNavigationSignal = null;
    var activeNavigationAbortHandler = null;
    var phase = 'idle';

    function getSessionStorage() {
        try {
            return window.sessionStorage || null;
        } catch (error) {
            return null;
        }
    }

    function writePendingNavigation() {
        var storage = getSessionStorage();
        if (!storage) {
            return;
        }
        try {
            storage.setItem(STORAGE_KEY, String(Date.now()));
        } catch (error) {
            // Storage is optional; current-page progress still works without it.
        }
    }

    function clearPendingNavigation() {
        var storage = getSessionStorage();
        if (!storage) {
            return;
        }
        try {
            storage.removeItem(STORAGE_KEY);
        } catch (error) {
            // Ignore unavailable or blocked session storage.
        }
    }

    function detachNavigationAbort() {
        if (activeNavigationSignal && activeNavigationAbortHandler &&
            typeof activeNavigationSignal.removeEventListener === 'function') {
            try {
                activeNavigationSignal.removeEventListener('abort', activeNavigationAbortHandler);
            } catch (error) {
                // Ignore signals that do not support listener removal.
            }
        }
        activeNavigationSignal = null;
        activeNavigationAbortHandler = null;
    }

    function readPendingNavigationTimestamp() {
        var storage = getSessionStorage();
        if (!storage) {
            return null;
        }

        try {
            var rawTimestamp = storage.getItem(STORAGE_KEY);
            if (rawTimestamp === null || String(rawTimestamp).trim() === '') {
                return null;
            }

            var timestamp = Number(rawTimestamp);
            var age = Date.now() - timestamp;
            if (timestamp !== timestamp || !isFinite(timestamp) || age < 0 || age >= SAFETY_TIMEOUT_MS) {
                storage.removeItem(STORAGE_KEY);
                return null;
            }
            return timestamp;
        } catch (error) {
            return null;
        }
    }

    function injectStyle() {
        if (document.getElementById(STYLE_ID)) {
            return;
        }

        var root = document.head || document.documentElement;
        if (!root) {
            return;
        }

        var style = document.createElement('style');
        style.id = STYLE_ID;
        style.textContent = CSS_TEXT;
        root.appendChild(style);
    }

    function ensureProgressBar() {
        if (container && container.isConnected && fill) {
            return true;
        }

        injectStyle();
        container = document.getElementById(CONTAINER_ID);
        if (container) {
            fill = container.querySelector('.' + FILL_CLASS);
            return Boolean(fill);
        }

        var root = document.body || document.documentElement;
        if (!root) {
            return false;
        }

        container = document.createElement('div');
        container.id = CONTAINER_ID;
        container.setAttribute('role', 'progressbar');
        container.setAttribute('aria-label', '页面加载进度');
        container.setAttribute('aria-valuemin', '0');
        container.setAttribute('aria-valuemax', '100');
        container.setAttribute('aria-valuenow', '0');
        container.setAttribute('aria-hidden', 'true');

        fill = document.createElement('div');
        fill.className = FILL_CLASS;
        container.appendChild(fill);
        root.appendChild(container);
        return true;
    }

    function setProgress(percent, duration) {
        if (!ensureProgressBar()) {
            return false;
        }

        fill.style.transitionDuration = Math.max(0, duration) + 'ms';
        fill.style.transform = 'scaleX(' + (percent / 100) + ')';
        container.setAttribute('aria-valuenow', String(percent));
        return true;
    }

    function showProgressBar() {
        if (!ensureProgressBar()) {
            return false;
        }
        container.setAttribute('data-visible', 'true');
        container.setAttribute('aria-hidden', 'false');
        return true;
    }

    function clearTimers() {
        window.clearTimeout(showTimer);
        window.clearTimeout(slowTimer);
        window.clearTimeout(safetyTimer);
        window.clearTimeout(fadeTimer);
        window.clearTimeout(resetTimer);
        window.clearTimeout(mountTimer);
        window.clearTimeout(completionTimer);
        showTimer = 0;
        slowTimer = 0;
        safetyTimer = 0;
        fadeTimer = 0;
        resetTimer = 0;
        mountTimer = 0;
        completionTimer = 0;
        pendingClickEvent = null;
    }

    function resetProgress() {
        clearTimers();
        clearPendingNavigation();
        detachNavigationAbort();
        phase = 'idle';

        if (!container) {
            return;
        }
        container.removeAttribute('data-visible');
        container.setAttribute('aria-hidden', 'true');
        setProgress(0, 0);
    }

    function scheduleSafetyReset(delay) {
        window.clearTimeout(safetyTimer);
        safetyTimer = window.setTimeout(resetProgress, delay === undefined ? SAFETY_TIMEOUT_MS : delay);
    }

    function beginProgress() {
        detachNavigationAbort();
        clearTimers();
        phase = 'loading';
        writePendingNavigation();

        if (showProgressBar()) {
            setProgress(0, 0);
            void fill.offsetWidth;
            setProgress(80, 300);
        }

        slowTimer = window.setTimeout(function () {
            slowTimer = 0;
            if (phase === 'loading') {
                setProgress(90, 1200);
            }
        }, SLOW_PROGRESS_DELAY_MS);
        scheduleSafetyReset();
    }

    function mountResumedProgress() {
        mountTimer = 0;
        if (phase !== 'resumed') {
            return;
        }
        if (showProgressBar()) {
            setProgress(90, 0);
            return;
        }
        mountTimer = window.setTimeout(mountResumedProgress, MOUNT_RETRY_MS);
    }

    function resumeProgress(startedAt) {
        clearTimers();
        detachNavigationAbort();
        phase = 'resumed';
        mountResumedProgress();
        scheduleSafetyReset(Math.max(0, SAFETY_TIMEOUT_MS - (Date.now() - startedAt)));
    }

    function scheduleHistoryCompletion() {
        window.clearTimeout(completionTimer);
        completionTimer = window.setTimeout(function () {
            completionTimer = 0;
            finishProgress();
        }, HISTORY_FINISH_DELAY_MS);
    }

    function finishProgress() {
        if (phase !== 'resumed') {
            return;
        }

        window.clearTimeout(safetyTimer);
        window.clearTimeout(mountTimer);
        window.clearTimeout(completionTimer);
        safetyTimer = 0;
        mountTimer = 0;
        completionTimer = 0;
        phase = 'finishing';
        detachNavigationAbort();
        clearPendingNavigation();
        showProgressBar();
        setProgress(100, COMPLETE_DURATION_MS);

        fadeTimer = window.setTimeout(function () {
            fadeTimer = 0;
            if (container) {
                container.removeAttribute('data-visible');
                container.setAttribute('aria-hidden', 'true');
            }

            resetTimer = window.setTimeout(function () {
                resetTimer = 0;
                phase = 'idle';
                setProgress(0, 0);
            }, FADE_DURATION_MS);
        }, COMPLETE_HOLD_MS);
    }

    function getNavigationUrl(event, currentHref) {
        if (!event || event.defaultPrevented || event.button !== 0 ||
            event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
            return null;
        }

        var target = event.target;
        if (target && target.nodeType !== 1) {
            target = target.parentElement;
        }
        if (!target || typeof target.closest !== 'function') {
            return null;
        }

        var link = target.closest('a[href]');
        if (!link || link.hasAttribute('download')) {
            return null;
        }

        var linkTarget = String(link.getAttribute('target') || '').trim().toLowerCase();
        if (linkTarget && linkTarget !== '_self') {
            return null;
        }

        var rawHref = link.getAttribute('href');
        if (rawHref === null) {
            return null;
        }
        rawHref = String(rawHref).trim();

        try {
            var currentUrl = new URL(currentHref);
            var navigationUrl = new URL(rawHref, currentUrl);
            if (navigationUrl.protocol !== 'https:' || navigationUrl.origin !== currentUrl.origin) {
                return null;
            }

            var sameResource = navigationUrl.pathname === currentUrl.pathname &&
                navigationUrl.search === currentUrl.search;
            var isFragmentNavigation = sameResource &&
                (rawHref.indexOf('#') >= 0 || Boolean(currentUrl.hash));
            return isFragmentNavigation ? null : navigationUrl;
        } catch (error) {
            return null;
        }
    }

    function isSameOriginHttpsUrl(rawUrl) {
        try {
            var currentUrl = new URL(window.location.href);
            var destinationUrl = new URL(rawUrl, currentUrl);
            return destinationUrl.protocol === 'https:' && destinationUrl.origin === currentUrl.origin;
        } catch (error) {
            return false;
        }
    }

    function watchNavigationAbort(event) {
        var signal = event && event.signal;
        if (!signal || typeof signal.addEventListener !== 'function') {
            return;
        }

        activeNavigationSignal = signal;
        activeNavigationAbortHandler = resetProgress;
        try {
            signal.addEventListener('abort', activeNavigationAbortHandler, { once: true });
            if (signal.aborted) {
                resetProgress();
            }
        } catch (error) {
            detachNavigationAbort();
        }
    }

    function handleNavigationEvent(event) {
        if (!event || event.navigationType !== 'traverse' || !event.destination ||
            event.destination.sameDocument !== false || !isSameOriginHttpsUrl(event.destination.url)) {
            return;
        }

        try {
            beginProgress();
            watchNavigationAbort(event);
        } catch (error) {
            resetProgress();
        }
    }

    function installNavigationListener() {
        try {
            if (window.navigation && typeof window.navigation.addEventListener === 'function') {
                window.navigation.addEventListener('navigate', handleNavigationEvent, false);
            }
        } catch (error) {
            // Navigation API is optional.
        }
    }

    function isBackForwardNavigation() {
        try {
            var performanceApi = window.performance;
            if (performanceApi && typeof performanceApi.getEntriesByType === 'function') {
                var entries = performanceApi.getEntriesByType('navigation');
                if (entries && entries[0] && entries[0].type === 'back_forward') {
                    return true;
                }
            }
            return Boolean(performanceApi && performanceApi.navigation && performanceApi.navigation.type === 2);
        } catch (error) {
            return false;
        }
    }

    function scheduleProgress(event) {
        if (phase === 'loading') {
            resetProgress();
        }
        window.clearTimeout(showTimer);
        pendingClickEvent = event;

        showTimer = window.setTimeout(function () {
            var clickEvent = pendingClickEvent;
            showTimer = 0;
            pendingClickEvent = null;
            if (!clickEvent || clickEvent.defaultPrevented) {
                return;
            }

            try {
                beginProgress();
            } catch (error) {
                resetProgress();
            }
        }, SHOW_DELAY_MS);
    }

    function handleDocumentClick(event) {
        if (getNavigationUrl(event, window.location.href)) {
            scheduleProgress(event);
        }
    }

    function handlePageShow(event) {
        if (!event.persisted) {
            return;
        }

        try {
            var startedAt = readPendingNavigationTimestamp();
            if (startedAt === null) {
                startedAt = Date.now();
            }
            resumeProgress(startedAt);
            scheduleHistoryCompletion();
        } catch (error) {
            resetProgress();
        }
    }

    document.addEventListener('click', handleDocumentClick, false);
    window.addEventListener('pageshow', handlePageShow, false);
    installNavigationListener();

    var pendingNavigationTimestamp = readPendingNavigationTimestamp();
    if (pendingNavigationTimestamp === null && isBackForwardNavigation()) {
        pendingNavigationTimestamp = Date.now();
    }
    if (pendingNavigationTimestamp !== null) {
        try {
            resumeProgress(pendingNavigationTimestamp);
            if (document.readyState === 'loading') {
                document.addEventListener('DOMContentLoaded', finishProgress, { once: true });
            } else {
                window.setTimeout(finishProgress, 0);
            }
        } catch (error) {
            resetProgress();
        }
    }
})();