Greasy Fork is available in English.

Reddit Scroll Brake

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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