LINUX.SB Navigation Progress

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

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

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

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

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

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

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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