Reddit Scroll Brake

Limit visible posts and comments on Reddit and reveal more on demand.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Reddit Scroll Brake
// @namespace    https://github.com/atharvj/reddit-scroll-brake
// @version      1.0.4
// @description  Limit visible posts and comments on Reddit and reveal more on demand.
// @license      MIT
// @match        https://*.reddit.com/*
// @match        https://www.reddit.com/*
// @match        https://reddit.com/*
// @run-at       document-idle
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  const INITIAL_VISIBLE_ITEMS = 10;
  const ITEMS_PER_CLICK = 5;
  const BRAKE_ID = "reddit-scroll-brake";
  const STYLE_ID = "reddit-scroll-brake-styles";
  const STORAGE_KEY = "reddit-scroll-brake-items-per-click";
  const SCAN_DELAY_MS = 75;
  const ROUTE_SETTLE_DELAY_MS = 300;
  const BRAKE_PANEL_HEIGHT = 86;
  const TOP_RESET_SCROLL_Y = 120;
  const TOP_RESET_MIN_DISTANCE = 600;
  const MIN_ITEMS_PER_CLICK = 1;
  const MAX_ITEMS_PER_CLICK = 100;
  const MIN_POST_HEIGHT = 48;
  const MIN_COMMENT_HEIGHT = 24;
  const EXPAND_LOCK_GRACE_MS = 1200;
  const MAX_SAVED_ROUTE_STATES = 12;

  let visibleItemLimit = getInitialVisibleItemLimit();
  let itemsPerClick = readSavedItemsPerClick();
  let currentRouteKey = getRouteKey();
  let scanTimer = null;
  let lastTouchY = null;
  let lastScrollY = window.scrollY;
  let routeSettlesAt = 0;
  let isLocked = false;
  let maxScrollY = null;
  let minScrollYForNextLock = null;
  let allowMaxScrollIncreaseUntil = 0;
  let currentRecordCount = 0;
  const orderedKeys = [];
  const routeStates = new Map();

  function addStyles() {
    if (document.getElementById(STYLE_ID)) {
      return;
    }

    const style = document.createElement("style");
    style.id = STYLE_ID;
    style.textContent = `
      #${BRAKE_ID} {
        position: fixed;
        left: 50%;
        bottom: 18px;
        z-index: 2147483647;
        box-sizing: border-box;
        width: min(520px, calc(100vw - 32px));
        transform: translateX(-50%);
        padding: 8px 10px;
        border: 1px solid var(--color-neutral-border-weak, var(--color-tone-5, #edeff1));
        border-radius: var(--radius-md, 20px);
        background: var(--color-neutral-background-strong, var(--color-tone-7, #ffffff));
        color: var(--color-neutral-content-strong, var(--color-tone-1, #0f1a1c));
        box-shadow: var(--boxshadow-modal, 0 12px 36px rgba(15, 26, 28, 0.22));
      }

      #${BRAKE_ID}[hidden] {
        display: none !important;
      }

      #${BRAKE_ID} .reddit-scroll-brake-controls {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 8px;
        flex-wrap: wrap;
        font: 15px/1.3 var(--font-sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
      }

      #${BRAKE_ID} button {
        border: 0;
        border-radius: 999px;
        padding: 8px 14px;
        background: var(--color-brand-background, var(--color-global-brand-orangered, #ff4500));
        color: white;
        font: 700 15px/1.2 var(--font-sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
        cursor: pointer;
      }

      #${BRAKE_ID} button:hover {
        background: var(--color-brand-background-hover, #d93a00);
      }

      #${BRAKE_ID} button:focus-visible,
      #${BRAKE_ID} input:focus-visible {
        outline: 3px solid var(--color-global-focus, rgba(71, 176, 219, 0.7));
        outline-offset: 3px;
      }

      #${BRAKE_ID} label {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        font-weight: 600;
      }

      #${BRAKE_ID} input {
        box-sizing: border-box;
        width: 64px;
        border: 1px solid var(--color-neutral-border, var(--color-tone-5, #edeff1));
        border-radius: var(--radius-sm, 8px);
        padding: 6px 8px;
        background: var(--color-neutral-background, var(--color-tone-7, #ffffff));
        color: inherit;
        font: 600 15px/1 var(--font-sans, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif);
      }

      #${BRAKE_ID} .reddit-scroll-brake-status {
        width: 100%;
        text-align: center;
        color: var(--color-neutral-content-weak, var(--color-tone-2, #576f76));
        font-size: 12px;
      }

      @media (prefers-color-scheme: dark) {
        #${BRAKE_ID} {
          box-shadow: 0 12px 36px rgba(0, 0, 0, 0.5);
        }
      }
    `;

    document.head.appendChild(style);
  }

  function clampWholeNumber(value, fallback, min, max) {
    const number = Number(value);

    if (!Number.isFinite(number)) {
      return fallback;
    }

    return Math.min(max, Math.max(min, Math.floor(number)));
  }

  function getInitialVisibleItemLimit() {
    return clampWholeNumber(INITIAL_VISIBLE_ITEMS, 10, 1, Number.MAX_SAFE_INTEGER);
  }

  function getRouteKey() {
    return `${location.pathname}${location.search}`;
  }

  function readSavedItemsPerClick() {
    let savedValue = null;
    try {
      savedValue = window.localStorage.getItem(STORAGE_KEY);
    } catch (error) {
      savedValue = null;
    }

    return clampWholeNumber(
      savedValue || ITEMS_PER_CLICK,
      ITEMS_PER_CLICK,
      MIN_ITEMS_PER_CLICK,
      MAX_ITEMS_PER_CLICK
    );
  }

  function saveItemsPerClick() {
    try {
      window.localStorage.setItem(STORAGE_KEY, String(itemsPerClick));
    } catch (error) {
      // Local storage may be unavailable.
    }
  }

  function isCommentsPage() {
    return /\/comments\//.test(location.pathname);
  }

  function getMainContent() {
    return (
      document.querySelector("main") ||
      document.querySelector('[role="main"]') ||
      document.querySelector("shreddit-app") ||
      document.body
    );
  }

  function getAttributeValue(element, names) {
    if (!(element instanceof Element)) {
      return "";
    }

    for (const name of names) {
      const value = element.getAttribute(name);

      if (value) {
        return value;
      }
    }

    return "";
  }

  function normalizeThingKey(value, prefix) {
    if (!value) {
      return "";
    }

    const normalized = String(value).trim();
    const thingMatch = normalized.match(new RegExp(`${prefix}_([a-z0-9]+)`, "i"));

    if (thingMatch) {
      return `${prefix}_${thingMatch[1]}`;
    }

    return normalized;
  }

  function getPostKeyFromHref(href) {
    if (!href) {
      return "";
    }

    try {
      const url = new URL(href, location.origin);
      const postMatch = url.pathname.match(/^\/r\/[^/]+\/comments\/([^/]+)/i);

      if (postMatch) {
        return `post:${postMatch[1]}`;
      }

      return "";
    } catch (error) {
      return "";
    }
  }

  function getCommentKeyFromHref(href) {
    if (!href) {
      return "";
    }

    try {
      const url = new URL(href, location.origin);
      const commentMatch = url.pathname.match(/^\/r\/[^/]+\/comments\/[^/]+\/[^/]+\/([^/?#]+)/i);

      if (commentMatch) {
        return `comment:${commentMatch[1]}`;
      }

      if (url.hash) {
        return `comment:${url.hash.replace(/^#/, "")}`;
      }

      return "";
    } catch (error) {
      return "";
    }
  }

  function getVisibleCountableRecords(records, minHeight) {
    return records.filter((record) => isCountableRecord(record.item, minHeight));
  }

  function isCountableRecord(element, minHeight) {
    if (!(element instanceof Element) || element.closest(`#${BRAKE_ID}`)) {
      return false;
    }

    if (
      element.hidden ||
      element.getAttribute("aria-hidden") === "true" ||
      element.closest(
        'aside, nav, header, footer, dialog, [aria-modal="true"], [role="complementary"]'
      )
    ) {
      return false;
    }

    const rect = element.getBoundingClientRect();

    if (rect.width < 180 || rect.height < minHeight) {
      return false;
    }

    const style = window.getComputedStyle(element);

    return style.display !== "none" && style.visibility !== "hidden";
  }

  function getDistinctCommentLinks(element) {
    const keys = new Set();

    element.querySelectorAll('a[href*="/comments/"]').forEach((link) => {
      const key = getPostKeyFromHref(link.getAttribute("href"));

      if (key) {
        keys.add(key);
      }
    });

    return keys;
  }

  function findPostContainerFromLink(link) {
    const key = getPostKeyFromHref(link.getAttribute("href"));

    if (!key) {
      return link;
    }

    let current = link.parentElement;
    let best = null;

    while (current && current !== document.body) {
      if (current.matches("main, [role='main'], shreddit-app")) {
        break;
      }

      const rect = current.getBoundingClientRect();

      if (rect.width >= 180 && rect.height >= MIN_POST_HEIGHT) {
        const linkedPostKeys = getDistinctCommentLinks(current);

        if (linkedPostKeys.size === 1 && linkedPostKeys.has(key)) {
          best = current;
        } else if (linkedPostKeys.size > 1) {
          break;
        }
      }

      current = current.parentElement;
    }

    return best || link;
  }

  function getPostRecords() {
    const root = getMainContent();

    if (!root) {
      return [];
    }

    const selectors = [
      "shreddit-post",
      '[data-fullname^="t3_"]',
      '[id^="t3_"]',
      'div[data-testid="post-container"]',
      'div[data-testid="post-content"]',
      'article[data-testid="post-container"]',
      'article[data-testid="post-content"]',
      'article[role="article"]',
      '.thing.link',
      'a[id^="post-title-"][href*="/comments/"]',
      'a[data-click-id="body"][href*="/comments/"]',
      'a[slot="title"][href*="/comments/"]',
    ];

    const seenItems = new Set();
    const seenKeys = new Set();
    const records = [];

    root.querySelectorAll(selectors.join(",")).forEach((element) => {
      const post = getPostElement(element);

      if (
        !post ||
        seenItems.has(post) ||
        !isCountableRecord(post, MIN_POST_HEIGHT)
      ) {
        return;
      }

      seenItems.add(post);
      const key = getPostKey(post);

      if (!key || seenKeys.has(key)) {
        return;
      }

      seenKeys.add(key);
      records.push({ item: post, key });
    });

    return getVisibleCountableRecords(records, MIN_POST_HEIGHT);
  }

  function getPostElement(element) {
    if (!(element instanceof Element)) {
      return null;
    }

    return (
      element.closest("shreddit-post") ||
      element.closest('[data-testid="post-container"]') ||
      element.closest('article[data-testid="post-container"]') ||
      element.closest('article[data-testid="post-content"]') ||
      element.closest('article[role="article"]') ||
      element.closest(".thing.link") ||
      element.closest('[data-fullname^="t3_"]') ||
      element.closest('[id^="t3_"]') ||
      (element.matches('a[href*="/comments/"]') ? findPostContainerFromLink(element) : null)
    );
  }

  function getCommentRecords() {
    const root = getMainContent();

    if (!root) {
      return [];
    }

    const selectors = [
      "shreddit-comment",
      '[data-fullname^="t1_"]',
      '[id^="t1_"]',
      'div[data-testid="comment"]',
      'div[data-test-id="comment"]',
      '.thing.comment',
    ];
    const seenItems = new Set();
    const seenKeys = new Set();
    const records = [];

    root.querySelectorAll(selectors.join(",")).forEach((element) => {
      const comment = getCommentElement(element);

      if (
        !comment ||
        seenItems.has(comment) ||
        !isCountableRecord(comment, MIN_COMMENT_HEIGHT)
      ) {
        return;
      }

      seenItems.add(comment);
      const key = getCommentKey(comment);

      if (!key || seenKeys.has(key)) {
        return;
      }

      seenKeys.add(key);
      records.push({ item: comment, key });
    });

    return getVisibleCountableRecords(records, MIN_COMMENT_HEIGHT);
  }

  function getCommentElement(element) {
    if (!(element instanceof Element)) {
      return null;
    }

    return (
      element.closest("shreddit-comment") ||
      element.closest('div[data-testid="comment"]') ||
      element.closest('div[data-test-id="comment"]') ||
      element.closest(".thing.comment") ||
      element.closest('[data-fullname^="t1_"]') ||
      element.closest('[id^="t1_"]') ||
      null
    );
  }

  function getPostKey(post) {
    const postId = getAttributeValue(post, [
      "post-id",
      "postid",
      "data-post-id",
    ]);

    if (postId) {
      return `post:${normalizeThingKey(postId, "t3").replace(/^t3_/, "")}`;
    }

    const thingId = getAttributeValue(post, [
      "thingid",
      "thing-id",
      "data-fullname",
      "data-thing-id",
    ]);

    if (thingId) {
      return `post:${normalizeThingKey(thingId, "t3").replace(/^t3_/, "")}`;
    }

    const elementId = normalizeThingKey(post.id, "t3");

    if (/^t3_/i.test(elementId)) {
      return `post:${elementId.replace(/^t3_/i, "")}`;
    }

    const permalink = getAttributeValue(post, [
      "permalink",
      "data-permalink",
      "href",
    ]);

    if (permalink) {
      return getPostKeyFromHref(permalink);
    }

    const link = Array.from(
      post.querySelectorAll('a[href*="/comments/"]')
    ).find((candidate) => getPostKeyFromHref(candidate.getAttribute("href")));

    if (link) {
      return getPostKeyFromHref(link.getAttribute("href"));
    }

    return "";
  }

  function getCommentKey(comment) {
    const commentId = getAttributeValue(comment, [
      "comment-id",
      "commentid",
      "data-comment-id",
    ]);

    if (commentId) {
      return `comment:${normalizeThingKey(commentId, "t1").replace(/^t1_/, "")}`;
    }

    const thingId = getAttributeValue(comment, [
      "thingid",
      "thing-id",
      "data-fullname",
      "data-thing-id",
    ]);

    if (thingId) {
      return `comment:${normalizeThingKey(thingId, "t1").replace(/^t1_/, "")}`;
    }

    const elementId = normalizeThingKey(comment.id, "t1");

    if (/^t1_/i.test(elementId)) {
      return `comment:${elementId.replace(/^t1_/i, "")}`;
    }

    const permalink = getAttributeValue(comment, [
      "permalink",
      "data-permalink",
      "href",
    ]);

    if (permalink) {
      return getCommentKeyFromHref(permalink);
    }

    const timestampLink = Array.from(
      comment.querySelectorAll('a[data-click-id="timestamp"], a[href*="/comments/"]')
    ).find((candidate) =>
      getCommentKeyFromHref(candidate.getAttribute("href")).startsWith("comment:")
    );

    if (timestampLink) {
      return getCommentKeyFromHref(timestampLink.getAttribute("href"));
    }

    return "";
  }

  function getCountableRecords() {
    return isCommentsPage() ? getCommentRecords() : getPostRecords();
  }

  function rememberItemOrder(records) {
    records.forEach((record, index) => {
      if (orderedKeys.includes(record.key)) {
        return;
      }

      const previousKnownKey = findNearbyKnownKey(records, index - 1, -1);
      const nextKnownKey = findNearbyKnownKey(records, index + 1, 1);

      if (previousKnownKey) {
        orderedKeys.splice(orderedKeys.indexOf(previousKnownKey) + 1, 0, record.key);
      } else if (nextKnownKey) {
        orderedKeys.splice(orderedKeys.indexOf(nextKnownKey), 0, record.key);
      } else {
        orderedKeys.push(record.key);
      }
    });
  }

  function findNearbyKnownKey(records, startIndex, direction) {
    for (
      let index = startIndex;
      index >= 0 && index < records.length;
      index += direction
    ) {
      if (orderedKeys.includes(records[index].key)) {
        return records[index].key;
      }
    }

    return "";
  }

  function applyBrake() {
    resetAfterNavigation();

    const settleTimeLeft = routeSettlesAt - Date.now();
    if (settleTimeLeft > 0) {
      scheduleApplyBrake(settleTimeLeft);
      return;
    }

    const records = getCountableRecords();
    currentRecordCount = records.length;
    rememberItemOrder(records);

    if (getKnownRecordCount() < visibleItemLimit) {
      unlockBrake();
      return;
    }

    const anchorRecord = findAnchorRecord(records);
    if (!anchorRecord) {
      unlockBrake();
      updateBrakeText();
      return;
    }

    lockAtRecord(anchorRecord, records);
  }

  function findAnchorRecord(records) {
    if (isCommentsPage()) {
      return records[visibleItemLimit - 1] || null;
    }

    const anchorKey = orderedKeys[visibleItemLimit - 1];
    if (!anchorKey) {
      return null;
    }

    const fromOrder = records.find((record) => record.key === anchorKey);
    if (fromOrder) {
      return fromOrder;
    }

    return records.find((record) => orderedKeys.indexOf(record.key) >= visibleItemLimit) || null;
  }

  function findFirstHiddenRecord(records) {
    if (isCommentsPage()) {
      return records[visibleItemLimit] || null;
    }

    return records.find((record) => orderedKeys.indexOf(record.key) >= visibleItemLimit) || null;
  }

  function getKnownRecordCount() {
    return isCommentsPage() ? currentRecordCount : orderedKeys.length;
  }

  function lockAtRecord(record, records) {
    let nextMaxScrollY = getMaxScrollYForRecord(record, records);

    if (minScrollYForNextLock !== null) {
      nextMaxScrollY = Math.max(nextMaxScrollY, minScrollYForNextLock);
      minScrollYForNextLock = null;
    }

    setMaxScrollY(nextMaxScrollY);
    isLocked = true;
    showBrake();
    updateBrakeText();
    clampScrollToBrake();
    pauseMediaAfterLimit();
  }

  function getMaxScrollYForRecord(record, records) {
    if (isCommentsPage()) {
      const firstHiddenRecord = findFirstHiddenRecord(records);

      if (firstHiddenRecord) {
        const rect = firstHiddenRecord.item.getBoundingClientRect();
        const hiddenTop = window.scrollY + rect.top;
        return Math.max(0, hiddenTop - window.innerHeight + BRAKE_PANEL_HEIGHT);
      }
    }

    const rect = record.item.getBoundingClientRect();
    const anchorBottom = window.scrollY + rect.bottom;
    return Math.max(0, anchorBottom - window.innerHeight + BRAKE_PANEL_HEIGHT);
  }

  function setMaxScrollY(nextMaxScrollY) {
    if (
      !isLocked ||
      maxScrollY === null ||
      nextMaxScrollY < maxScrollY ||
      Date.now() <= allowMaxScrollIncreaseUntil
    ) {
      maxScrollY = nextMaxScrollY;
    }
  }

  function unlockBrake() {
    isLocked = false;
    maxScrollY = null;
    hideBrake();
  }

  function ensureBrake() {
    const existingBrake = document.getElementById(BRAKE_ID);
    if (existingBrake) {
      return existingBrake;
    }

    const brake = document.createElement("div");
    brake.id = BRAKE_ID;
    brake.hidden = true;
    brake.innerHTML = `
      <div class="reddit-scroll-brake-controls">
        <button type="button">Load more</button>
        <label>
          <span>Items</span>
          <input type="number" min="${MIN_ITEMS_PER_CLICK}" max="${MAX_ITEMS_PER_CLICK}" step="1" inputmode="numeric">
        </label>
        <div class="reddit-scroll-brake-status" aria-live="polite"></div>
      </div>
    `;

    const input = brake.querySelector("input");
    const button = brake.querySelector("button");

    input.value = String(itemsPerClick);
    input.addEventListener("change", syncItemsPerClickFromInput);
    input.addEventListener("blur", syncItemsPerClickFromInput);
    input.addEventListener("input", updateBrakeText);

    button.addEventListener("click", () => {
      resetAfterNavigation();
      clearScheduledScan();
      syncItemsPerClickFromInput();
      minScrollYForNextLock = isCommentsPage() ? null : window.scrollY;
      allowMaxScrollIncreaseUntil = Date.now() + EXPAND_LOCK_GRACE_MS;
      visibleItemLimit += itemsPerClick;
      unlockBrake();
      scheduleApplyBrake();
    });

    document.body.appendChild(brake);
    return brake;
  }

  function showBrake() {
    ensureBrake().hidden = false;
  }

  function hideBrake() {
    const brake = document.getElementById(BRAKE_ID);
    if (brake) {
      brake.hidden = true;
    }
  }

  function syncItemsPerClickFromInput() {
    const input = document.querySelector(`#${BRAKE_ID} input`);
    if (!input) {
      return;
    }

    itemsPerClick = clampWholeNumber(
      input.value,
      itemsPerClick,
      MIN_ITEMS_PER_CLICK,
      MAX_ITEMS_PER_CLICK
    );
    input.value = String(itemsPerClick);
    saveItemsPerClick();
    updateBrakeText();
  }

  function updateBrakeText() {
    const brake = ensureBrake();
    const input = brake.querySelector("input");
    const button = brake.querySelector("button");
    const status = brake.querySelector(".reddit-scroll-brake-status");
    const typedCount = input ? input.value : itemsPerClick;
    const previewCount = clampWholeNumber(typedCount, itemsPerClick, MIN_ITEMS_PER_CLICK, MAX_ITEMS_PER_CLICK);
    const itemLabel = isCommentsPage() ? "comments" : "posts";
    const shownCount = Math.min(visibleItemLimit, getKnownRecordCount());
    const buttonText = `Load ${previewCount} more ${itemLabel}`;

    if (button && button.textContent !== buttonText) {
      button.textContent = buttonText;
    }

    if (button) {
      button.disabled = false;
    }

    if (input && document.activeElement !== input) {
      input.value = String(itemsPerClick);
    }

    if (status && status.textContent !== `Showing ${shownCount} ${itemLabel}`) {
      status.textContent = `Showing ${shownCount} ${itemLabel}`;
    }
  }

  function pauseMediaAfterLimit() {
    const records = getCountableRecords();
    records.forEach((record) => {
      const keyIndex = orderedKeys.indexOf(record.key);
      if (keyIndex >= visibleItemLimit) {
        pauseMediaInElement(record.item);
      }
    });
  }

  function pauseMediaInElement(element) {
    element.querySelectorAll("audio, video").forEach((media) => {
      media.pause();
      media.autoplay = false;
      media.removeAttribute("autoplay");
    });
  }

  function scheduleApplyBrake(delay) {
    if (scanTimer !== null) {
      return;
    }

    const routeChanged = resetAfterNavigation();
    const wait = routeChanged
      ? ROUTE_SETTLE_DELAY_MS
      : clampWholeNumber(delay, SCAN_DELAY_MS, 0, ROUTE_SETTLE_DELAY_MS);

    scanTimer = window.setTimeout(() => {
      scanTimer = null;
      applyBrake();
    }, wait);
  }

  function clearScheduledScan() {
    if (scanTimer === null) {
      return;
    }
    window.clearTimeout(scanTimer);
    scanTimer = null;
  }

  function resetAfterNavigation() {
    const nextRouteKey = getRouteKey();
    if (nextRouteKey === currentRouteKey) {
      return false;
    }

    saveCurrentRouteState();
    currentRouteKey = nextRouteKey;

    if (!restoreRouteState(nextRouteKey)) {
      resetCurrentProgress();
    }

    return true;
  }

  function saveCurrentRouteState() {
    routeStates.delete(currentRouteKey);
    routeStates.set(currentRouteKey, {
      visibleItemLimit,
      currentRecordCount,
      orderedKeys: orderedKeys.slice(),
    });

    while (routeStates.size > MAX_SAVED_ROUTE_STATES) {
      const oldestKey = routeStates.keys().next().value;
      routeStates.delete(oldestKey);
    }
  }

  function restoreRouteState(routeKey) {
    const state = routeStates.get(routeKey);

    if (!state) {
      return false;
    }

    visibleItemLimit = clampWholeNumber(
      state.visibleItemLimit,
      getInitialVisibleItemLimit(),
      1,
      Number.MAX_SAFE_INTEGER
    );
    currentRecordCount = clampWholeNumber(
      state.currentRecordCount,
      0,
      0,
      Number.MAX_SAFE_INTEGER
    );
    orderedKeys.length = 0;
    state.orderedKeys.forEach((key) => {
      if (key && !orderedKeys.includes(key)) {
        orderedKeys.push(key);
      }
    });
    routeSettlesAt = Date.now() + ROUTE_SETTLE_DELAY_MS;
    minScrollYForNextLock = null;
    allowMaxScrollIncreaseUntil = 0;
    clearScheduledScan();
    unlockBrake();
    return true;
  }

  function resetCurrentProgress() {
    routeStates.delete(currentRouteKey);
    visibleItemLimit = getInitialVisibleItemLimit();
    routeSettlesAt = Date.now() + ROUTE_SETTLE_DELAY_MS;
    minScrollYForNextLock = null;
    allowMaxScrollIncreaseUntil = 0;
    currentRecordCount = 0;
    orderedKeys.length = 0;
    clearScheduledScan();
    unlockBrake();
  }

  function clampScrollToBrake() {
    const currentScrollY = window.scrollY;

    if (resetAfterNavigation()) {
      lastScrollY = currentScrollY;
      scheduleApplyBrake(ROUTE_SETTLE_DELAY_MS);
      return;
    }

    if (shouldResetAfterTopJump(currentScrollY)) {
      resetCurrentProgress();
      lastScrollY = currentScrollY;
      scheduleApplyBrake(ROUTE_SETTLE_DELAY_MS);
      return;
    }

    if (!isLocked || maxScrollY === null || window.scrollY <= maxScrollY) {
      lastScrollY = window.scrollY;
      return;
    }

    window.scrollTo({
      top: maxScrollY,
      left: window.scrollX,
      behavior: "auto",
    });

    lastScrollY = maxScrollY;
  }

  function shouldResetAfterTopJump(currentScrollY) {
    return (
      isLocked &&
      maxScrollY !== null &&
      currentScrollY <= TOP_RESET_SCROLL_Y &&
      (maxScrollY >= TOP_RESET_MIN_DISTANCE || lastScrollY - currentScrollY >= TOP_RESET_MIN_DISTANCE)
    );
  }

  function preventScrollPastBrake(event) {
    if (resetAfterNavigation()) {
      scheduleApplyBrake(ROUTE_SETTLE_DELAY_MS);
      return;
    }

    if (!isLocked || maxScrollY === null || isEditableTarget(event.target)) {
      return;
    }

    const deltaY = getScrollDeltaY(event);
    if (deltaY <= 0 || window.scrollY + deltaY <= maxScrollY) {
      return;
    }

    event.preventDefault();

    if (window.scrollY < maxScrollY) {
      window.scrollTo({
        top: maxScrollY,
        left: window.scrollX,
        behavior: "auto",
      });
    }
  }

  function isEditableTarget(target) {
    if (!(target instanceof Element)) {
      return false;
    }

    return Boolean(target.closest('input, textarea, select, [contenteditable="true"]'));
  }

  function getScrollDeltaY(event) {
    if (event.type === "wheel") {
      if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) {
        return event.deltaY * 16;
      }
      if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
        return event.deltaY * window.innerHeight;
      }
      return event.deltaY;
    }

    if (event.type === "touchmove") {
      const touch = event.touches[0];
      if (!touch || lastTouchY === null) {
        return 0;
      }
      return lastTouchY - touch.clientY;
    }

    if (event.type !== "keydown") {
      return 0;
    }

    const keyDeltas = {
      ArrowDown: 40,
      PageDown: window.innerHeight * 0.85,
      End: Number.MAX_SAFE_INTEGER,
      " ": window.innerHeight * 0.85,
    };

    return keyDeltas[event.key] || 0;
  }

  function rememberTouchPosition(event) {
    const touch = event.touches[0];
    lastTouchY = touch ? touch.clientY : null;
  }

  function forgetTouchPosition() {
    lastTouchY = null;
  }

  function handleProgressResetClick(event) {
    if (!shouldResetProgressForClick(event.target)) {
      return;
    }

    resetCurrentProgress();
    scheduleApplyBrake(ROUTE_SETTLE_DELAY_MS);
  }

  function shouldResetProgressForClick(target) {
    if (!(target instanceof Element)) {
      return false;
    }

    if (target.closest(`#${BRAKE_ID}`)) {
      return false;
    }

    if (
      target.closest(
        'a[href="/"], a[href="/home"], a[href$="/home"], a[href$="/best"], a[href$="/new"]'
      )
    ) {
      return true;
    }

    const control = target.closest('button, [role="button"], a');
    if (!control) {
      return false;
    }

    const label = `${control.getAttribute("aria-label") || ""} ${control.textContent || ""}`;
    return /\b(home|top|best|new|comments|refresh)\b/i.test(label);
  }

  function handleTopResetShortcut(event) {
    if (isEditableTarget(event.target)) {
      return;
    }

    if (event.key !== "Home" && !((event.metaKey || event.ctrlKey) && event.key === "ArrowUp")) {
      return;
    }

    resetCurrentProgress();
    scheduleApplyBrake(ROUTE_SETTLE_DELAY_MS);
  }

  function installNavigationHooks() {
    const notifyNavigation = () => {
      if (resetAfterNavigation()) {
        scheduleApplyBrake(ROUTE_SETTLE_DELAY_MS);
      }
    };

    ["pushState", "replaceState"].forEach((methodName) => {
      const originalMethod = history[methodName];
      if (typeof originalMethod !== "function") {
        return;
      }
      history[methodName] = function () {
        const result = originalMethod.apply(this, arguments);
        window.setTimeout(notifyNavigation, 0);
        return result;
      };
    });

    window.addEventListener("popstate", notifyNavigation);
  }

  function boot() {
    addStyles();
    ensureBrake();
    installNavigationHooks();
    applyBrake();

    const observer = new MutationObserver(() => {
      scheduleApplyBrake();
    });
    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });

    window.addEventListener("scroll", clampScrollToBrake, { passive: true });
    window.addEventListener("click", handleProgressResetClick, {
      capture: true,
      passive: true,
    });
    window.addEventListener("wheel", preventScrollPastBrake, {
      capture: true,
      passive: false,
    });
    window.addEventListener("keydown", handleTopResetShortcut, {
      capture: true,
    });
    window.addEventListener("keydown", preventScrollPastBrake, {
      capture: true,
    });
    window.addEventListener("touchstart", rememberTouchPosition, {
      capture: true,
      passive: true,
    });
    window.addEventListener("touchmove", preventScrollPastBrake, {
      capture: true,
      passive: false,
    });
    window.addEventListener("touchmove", rememberTouchPosition, {
      capture: true,
      passive: true,
    });
    window.addEventListener("touchend", forgetTouchPosition, {
      capture: true,
      passive: true,
    });
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", boot, { once: true });
  } else {
    boot();
  }
})();