为站内页面跳转显示轻量的顶部进度条,不拦截原生导航。
Ajankohdalta
// ==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();
}
}
})();