LINUX.SB Navigation Progress

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

Version au 21/08/2026. Voir la dernière version.

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

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

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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