Greasy Fork 还支持 简体中文。

T3 Chat Zoomed-Out Scrollbar

Adds a zoomed-out preview scrollbar to T3 Chat, showing only the chat log.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         T3 Chat Zoomed-Out Scrollbar
// @namespace    http://tampermonkey.net/
// @version      2.6
// @description  Adds a zoomed-out preview scrollbar to T3 Chat, showing only the chat log.
// @author       Dimava, T3 Chat, Gemini 2.0 Flash
// @match        https://t3.chat/*
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  "use strict";

  const PREVIEW_SCROLLBAR_ID = "t3-chat-preview-scrollbar";
  const PREVIEW_CONTENT_ID = "t3-chat-preview-content";
  const PREVIEW_THUMB_ID = "t3-chat-preview-thumb";
  const PREVIEW_SCALE = 0.1; // Scale factor for the preview
  const THUMB_HEIGHT_VH = 10; // Thumb height in viewport height units
  const SCROLLBAR_OFFSET_PX = 20; // Offset from the right edge
  const THROTTLE_DELAY_MS = 3000; // Throttle updates to every 3 seconds

  let lastContentUpdate = 0; // Timestamp of the last content update
  let scrollParent;
  let previewContent;
  let contentObserver;
  let thumb;
  let logDiv;
  let scrollbar;

  // Cleanup function
  function cleanup() {
    if (scrollbar) scrollbar.remove();
    window.removeEventListener("resize", updatePreviewContent);
    if (scrollParent)
      scrollParent.removeEventListener("scroll", updateThumbPosition);
    stopHeightPolling();
    if (contentObserver) contentObserver.disconnect();
  }

  // Call cleanup if it exists on the window
  if (window.t3ChatPreviewScrollbarCleanup) {
    window.t3ChatPreviewScrollbarCleanup();
  }

  // Function to update the preview content (throttled)
  function updatePreviewContent() {
    if (!logDiv || !previewContent) return;
    const now = Date.now();
    if (now - lastContentUpdate >= THROTTLE_DELAY_MS) {
      // Clone the logDiv content
      const clonedLogDiv = logDiv.cloneNode(true);

      // Apply styles to remove interactive elements
      const allElements = clonedLogDiv.querySelectorAll("*");
      allElements.forEach((el) => {
        el.style.pointerEvents = "none";
        el.style.userSelect = "none";
      });

      // Clear existing content and append the cloned content
      while (previewContent.firstChild) {
        previewContent.removeChild(previewContent.firstChild);
      }
      previewContent.appendChild(clonedLogDiv);

      // Set preview content width
      const logWidth = logDiv.getBoundingClientRect().width;
      previewContent.style.width = `${logWidth}px`;

      lastContentUpdate = now;
    }
  }

  // Function to update the thumb position
  function updateThumbPosition() {
    if (!logDiv || !scrollParent || !thumb || !scrollbar || !previewContent)
      return;

    const scrollHeight = scrollParent.scrollHeight;
    const clientHeight = scrollParent.clientHeight;
    const scrollTop = scrollParent.scrollTop;

    const scrollbarHeight = scrollbar.offsetHeight;
    const thumbHeightPx = (THUMB_HEIGHT_VH / 100) * window.innerHeight; // Constant thumb height

    let thumbPosition =
      (scrollbarHeight - thumbHeightPx) * (scrollTop / scrollHeight); // Proportional thumb position

    // Correct thumb position to allow scrolling to the very bottom
    thumbPosition = Math.min(
      thumbPosition,
      scrollbarHeight - thumbHeightPx
    );

    thumb.style.height = `${thumbHeightPx}px`;
    thumb.style.top = `${thumbPosition}px`;

    // Move the preview content up to align with the thumb
    const thumbCenter = thumbPosition + thumbHeightPx / 2;
    previewContent.style.top = `-${scrollTop * PREVIEW_SCALE - thumbCenter}px`;
  }

  // Function to handle thumb dragging
  function handleThumbDrag(event) {
    if (!logDiv || !scrollParent || !thumb || !scrollbar || !previewContent)
      return;

    const scrollHeight = scrollParent.scrollHeight;
    const clientHeight = scrollParent.clientHeight;
    const scrollbarHeight = scrollbar.offsetHeight;

    let startY = event.clientY;
    let startTop = thumb.offsetTop;

    function drag(e) {
      const deltaY = e.clientY - startY;
      let newTop = startTop + deltaY;
      const h = (THUMB_HEIGHT_VH / 100) * window.innerHeight;

      // Keep thumb within scrollbar bounds
      newTop = Math.max(0, Math.min(newTop, scrollbarHeight - thumb.offsetHeight));

      thumb.style.top = `${newTop}px`;

      // Calculate scroll position based on thumb position
      let scrollPosition = (newTop / (scrollbarHeight - h)) * scrollHeight;

      // Correct scroll position to allow scrolling to the very bottom
      scrollPosition = Math.min(
        scrollPosition,
        scrollHeight - clientHeight
      );

      scrollParent.scrollTop = scrollPosition;

      // Move the preview content up to align with the thumb
      const thumbCenter = newTop + (THUMB_HEIGHT_VH / 100) * window.innerHeight / 2;
      previewContent.style.top = `-${scrollPosition * PREVIEW_SCALE - thumbCenter}px`;
    }

    function stopDrag() {
      document.removeEventListener("mousemove", drag);
      document.removeEventListener("mouseup", stopDrag);
    }

    document.addEventListener("mousemove", drag);
    document.addEventListener("mouseup", stopDrag);

    event.preventDefault(); // Prevent text selection during drag
  }

  // Function to create the preview scrollbar
  function createPreviewScrollbar() {
    scrollbar = document.createElement("div");
    scrollbar.id = PREVIEW_SCROLLBAR_ID;
    scrollbar.style.cssText = `
            position: fixed;
            top: 0;
            right: ${SCROLLBAR_OFFSET_PX}px; /* Offset from the right edge */
            width: 50px; /* Initial width, will be updated */
            height: 100vh;
            background-color: rgba(0, 0, 0, 0.1);
            overflow: hidden;
            z-index: 1000;
        `;

    previewContent = document.createElement("div");
    previewContent.id = PREVIEW_CONTENT_ID;
    previewContent.style.cssText = `
            position: relative;
            width: 100%; /* Initial width, will be updated */
            height: auto;
            transform: scale(${PREVIEW_SCALE});
            transform-origin: top left;
            overflow: hidden;
            pointer-events: none;
            top: 0; /* Initial top position */
        `;
    scrollbar.appendChild(previewContent);

    thumb = document.createElement("div");
    thumb.id = PREVIEW_THUMB_ID;
    thumb.style.cssText = `
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: ${THUMB_HEIGHT_VH}vh; /* Constant thumb height */
            background-color: rgba(66, 135, 245, 0.5);
            cursor: grab;
        `;
    scrollbar.appendChild(thumb);

    document.body.appendChild(scrollbar);

    // Event listener for thumb dragging
    thumb.addEventListener("mousedown", handleThumbDrag);

    // Event listener for scrollbar mousedown
    scrollbar.addEventListener("mousedown", handleScrollbarMousedown);
  }

  // Function to handle scrollbar click
  function handleScrollbarMousedown(event) {
    if (event.target === thumb) return; // Ignore clicks on the thumb
    if (!logDiv || !scrollParent || !thumb || !scrollbar) return;

    const scrollbarHeight = scrollbar.offsetHeight;
    const clickY = event.clientY - scrollbar.getBoundingClientRect().top;

    const thumbHeightPx = (THUMB_HEIGHT_VH / 100) * window.innerHeight;
    const thumbPosition = thumb.offsetTop;
    const thumbCenter = thumbPosition + thumbHeightPx / 2;

    // Calculate the difference between the click position and the thumb center
    const deltaY = clickY - thumbCenter;

    // Adjust the scroll position by the scaled difference
    scrollParent.scrollTop += deltaY / PREVIEW_SCALE;

    // Ensure scroll position stays within bounds
    scrollParent.scrollTop = Math.max(
      0,
      Math.min(scrollParent.scrollTop, scrollParent.scrollHeight - scrollParent.clientHeight)
    );

    // Update thumb position immediately
    updateThumbPosition();
  }

  let heightPollingInterval;

  function pollElementHeight() {
    if (!logDiv || !scrollbar || !previewContent) return;
    const logWidth = logDiv.getBoundingClientRect().width;
    const scrollbarWidth = logWidth / 10;
    scrollbar.style.width = `${scrollbarWidth}px`;
    previewContent.style.width = `${logWidth}px`;
    requestAnimationFrame(pollElementHeight);
  }

  function startHeightPolling() {
    heightPollingInterval = requestAnimationFrame(pollElementHeight);
  }

  function stopHeightPolling() {
    cancelAnimationFrame(heightPollingInterval);
  }

  function initialize() {
    // Initialize
    createPreviewScrollbar();

    // Get elements
    logDiv = document.querySelector('[role="log"]');
    if (!logDiv) return;

    scrollParent = logDiv.parentNode;

    // Initial update
    updatePreviewContent();
    updateThumbPosition();

    // Event listeners
    contentObserver = new MutationObserver(() => {
      updatePreviewContent();
    });

    contentObserver.observe(logDiv, {
      childList: true,
      subtree: true,
      characterData: true,
    });

    window.addEventListener("resize", updatePreviewContent);

    if (!scrollParent) return;

    scrollParent.addEventListener("scroll", updateThumbPosition);

    // Start polling for height changes
    startHeightPolling();
  }

  function waitForLogDiv() {
    logDiv = document.querySelector('[role="log"]');
    if (logDiv) {
      initialize();
    } else {
      setTimeout(waitForLogDiv, 200); // Check every 200ms
    }
  }

  // Attach cleanup function to window
  window.t3ChatPreviewScrollbarCleanup = () => {
    cleanup();
    delete window.t3ChatPreviewScrollbarCleanup;
  };

  // Start waiting for the log div
  waitForLogDiv();

  // Public functions
  window.updatePreview = updatePreviewContent;
  window.updateThumb = updateThumbPosition;
})();