LINUX.SB Navigation Progress

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

Versión del día 21/8/2026. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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();
        }
    }
})();