LINUX.SB Navigation Progress

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

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

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