Overleaf Space Maximiser

(Requires Tampermonkey Legacy / MV2 on Chromium!) Auto-hide Overleaf top toolbar to maximise vertical space. Hover over that area to show it again. To optionally maximise horizontal space, you can optimise file tree/outline spacing and/or hide file outline to maximise horizontal space. Toggle with settings button in toolbar.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        Overleaf Space Maximiser
// @namespace   https://www.github.com/sjain882
// @author      sjain882 / shanie
// @match       https://www.overleaf.com/project/*
// @version     0.4.2
// @icon        https://www.google.com/s2/favicons?sz=64&domain=overleaf.com
// @description (Requires Tampermonkey Legacy / MV2 on Chromium!) Auto-hide Overleaf top toolbar to maximise vertical space. Hover over that area to show it again. To optionally maximise horizontal space, you can optimise file tree/outline spacing and/or hide file outline to maximise horizontal space. Toggle with settings button in toolbar.
// @homepageURL https://www.github.com/sjain882/Browser-Tweaks/Userscripts
// @supportURL  https://www.github.com/sjain882/Browser-Tweaks/issues
// @require     https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM.getValue
// @grant       GM.setValue
// @require     http://code.jquery.com/jquery-3.7.1.min.js
// @license MIT
// ==/UserScript==

/* globals $, GM_config */

(function () {
  "use strict";

  var $jqueryOverleafUserScript = jQuery.noConflict();
  // $j is now an alias to the jQuery function; creating the new alias is optional.

  var setIntervalFileTree;

  let gmc = new GM_config({
    id: "OverleafMaximiserConfig", // The id used for this instance of GM_config
    title: "Settings (WARNING: Saving will reload page!)", // Panel Title

    // Fields object
    fields: {

      // This is the id of the field
      HIDE_FILE_OUTLINE: {
        label: "Hide File Outline", // Appears next to field
        type: "checkbox", // Makes this setting a checkbox
        default: true, // Default value if user doesn't change it
      },

      OPTIMISE_FILE_TREE_SPACING: {
        label: "Optimise File Tree Spacing", 
        type: "checkbox", 
        default: true, 
      },

      FILE_TREE_FONT_SIZE: {
        label: "File Tree Font Size", 
        type: "text",
        default: "8", 
      },

      OPTIMISE_FILE_OUTLINE_SPACING: {
        label: "Optimise File Outline Spacing", 
        type: "checkbox", 
        default: true, 
      },

      FILE_OUTLINE_FONT_SIZE: {
        label: "File Outline Font Size", 
        type: "text", 
        default: "8", 
      }
    },

    events: {
      init: function () {
        // runs after initialization completes
        // override saved value
        this.set(
          "HIDE_FILE_OUTLINE",
          localStorage.getItem("ls_HIDE_FILE_OUTLINE") === "true"
        );
        this.set(
          "OPTIMISE_FILE_TREE_SPACING",
          localStorage.getItem("ls_OPTIMISE_FILE_TREE_SPACING") === "true"
        );
        this.set(
          "FILE_TREE_FONT_SIZE",
          localStorage.getItem("ls_FILE_TREE_FONT_SIZE") || "8"
        );
        this.set(
          "OPTIMISE_FILE_OUTLINE_SPACING",
          localStorage.getItem("ls_OPTIMISE_FILE_OUTLINE_SPACING") === "true"
        );
        this.set(
          "FILE_OUTLINE_FONT_SIZE",
          localStorage.getItem("ls_FILE_OUTLINE_FONT_SIZE") || "8"
        );

        optimiseFileTree();
        optimiseFileOutline();
        hideFileOutline();
        // gmc.open();
      },
      save: function () {
        localStorage.setItem(
          "ls_HIDE_FILE_OUTLINE",
          gmc.get("HIDE_FILE_OUTLINE")
        );
        localStorage.setItem(
          "ls_OPTIMISE_FILE_TREE_SPACING",
          gmc.get("OPTIMISE_FILE_TREE_SPACING")
        );
        localStorage.setItem(
          "ls_FILE_TREE_FONT_SIZE",
          gmc.get("FILE_TREE_FONT_SIZE")
        );
        localStorage.setItem(
          "ls_OPTIMISE_FILE_OUTLINE_SPACING",
          gmc.get("OPTIMISE_FILE_OUTLINE_SPACING")
        );
        localStorage.setItem(
          "ls_FILE_OUTLINE_FONT_SIZE",
          gmc.get("FILE_OUTLINE_FONT_SIZE")
        );
        optimiseFileTree();
        hideFileOutline();
        optimiseFileOutline();
        window.location.reload();
      },
    },
  });

  GM_addStyle(`
    nav.toolbar.toolbar-header {
      transition: transform 0.25s ease-in-out, opacity 0.25s ease-in-out;
      opacity: 1;
      z-index: 1000;
    }
    nav.toolbar.toolbar-header.toolbar-hidden {
      transform: translateY(-100%);
      opacity: 0;
      pointer-events: none;
      position: absolute !important; /* take it out of flex flow */
      top: 0;
      left: 0;
      right: 0;
    }

    .ide-react-body {
      flex: 1 1 auto !important; /* always expand to fill */
      transition: all 0.25s ease-in-out;
    }

  `);

  function makeButton() {
    const a = document.createElement("a");
    a.className =
      "d-inline-grid toolbar-header-upgrade-prompt btn btn-primary btn-sm";
    a.setAttribute("tabindex", "0");
    a.setAttribute("href", "#"); // prevents navigation
    a.setAttribute("role", "button");
    a.dataset.osmSettings = "1"; // marker so we don't duplicate
    const span = document.createElement("span");
    span.className = "button-content";
    span.setAttribute("aria-hidden", "false");
    span.textContent = "Overleaf Space Maximiser";
    a.appendChild(span);

    a.addEventListener("click", (ev) => {
      ev.preventDefault();
      try {
        gmc.open(); // open the GM_config UI from inside the userscript scope
      } catch (err) {
        console.error("Failed to open GM_config:", err);
        alert("Could not open settings UI — see console for details.");
      }
    });

    return a;
  }

  function insertIntoContainer(container) {
    if (!container) return false;
    // avoid duplicates
    if (container.querySelector('a[data-osm-settings="1"]')) return true;
    const btn = makeButton();

    // insert before the first "Upgrade" button if present, otherwise append
    const firstUpgrade = container.querySelector(
      "a.toolbar-header-upgrade-prompt"
    );
    if (firstUpgrade) container.insertBefore(btn, firstUpgrade);
    else container.appendChild(btn);

    return true;
  }

  function tryFindAndInsert() {
    // prefer targeting the nav by aria-label so it's less brittle
    const nav =
      document.querySelector('nav[aria-label="Project actions"]') ||
      document.querySelector("nav.toolbar.toolbar-header") ||
      document.querySelector("nav.toolbar-header") ||
      document.querySelector("nav.toolbar");
    if (!nav) return false;

    // direct-child selector to match your pasted markup:
    const container =
      nav.querySelector("div.d-flex.align-items-center") ||
      nav.querySelector(".d-flex.align-items-center");
    return insertIntoContainer(container);
  }

  // try immediately (in case element already present)
  if (!tryFindAndInsert()) {
    // element not found yet — observe DOM until the toolbar appears
    const mo = new MutationObserver((mutations, observer) => {
      if (tryFindAndInsert()) {
        observer.disconnect();
      }
    });
    mo.observe(document.documentElement || document.body, {
      childList: true,
      subtree: true,
    });
    // also set a fallback timeout to stop observing after 30s
    setTimeout(() => mo.disconnect(), 30000);
  }

  function optimiseFileTree() {
    if (gmc.get("OPTIMISE_FILE_TREE_SPACING")) {
      GM_addStyle(`
        #panel-file-tree > div > div.file-tree-inner {
          font-size: ${gmc.get("FILE_TREE_FONT_SIZE")}pt !important;
        }

        .item-name-button {
          padding-right: 0 !important;
        }
      `);
    }
  }

  function optimiseFileOutline() {
    if (gmc.get("OPTIMISE_FILE_OUTLINE_SPACING")) {
      GM_addStyle(`
        .outline-pane {
          font-size: ${gmc.get("FILE_OUTLINE_FONT_SIZE")}pt !important;
        }
      `);
    }
  }

  function collapsePanels() {
    const separator = document.querySelector(
      '[role="separator"][aria-controls="panel-file-tree"]'
    );
    if (!separator) return;

    // Force aria-valuenow to 0
    separator.setAttribute("aria-valuenow", "0");

    // Collapse the file tree panel
    const fileTreePanel = document.querySelector("#panel-file-tree");
    if (fileTreePanel) {
      fileTreePanel.style.flexBasis = "0px";
      fileTreePanel.style.height = "0px";
      fileTreePanel.style.minHeight = "0px";
      fileTreePanel.style.overflow = "hidden";
    }

    // Expand remaining panels if needed
    const otherPanels = document.querySelectorAll(
      '[data-panel-group-id=":r3:"] > div'
    );
    otherPanels.forEach((panel) => {
      if (panel !== separator && panel.id !== "panel-file-tree") {
        panel.style.flexGrow = "1";
      }
    });
  }

  function hideFileOutline() {
    if (gmc.get("HIDE_FILE_OUTLINE")) {
      GM_addStyle(`

            .outline-pane {
              position: absolute !important;
              width: 0 !important;
              height: 0 !important;
              overflow: hidden !important;
            }

            .outline-container {
              position: absolute !important;
              width: 0 !important;
              height: 0 !important;
              overflow: hidden !important;
            }

            .vertical-resize-handle {
              position: absolute !important;
              width: 0 !important;
              height: 0 !important;
              overflow: hidden !important;
            }

            .file-tree
            {
              height: 100% !important;
            }
            `);

      // Reapply every 500ms to override layout JS
      setIntervalFileTree = setInterval(collapsePanels, 500);
    }
  }

  function setupToolbarHider(toolbar) {
    if (!toolbar) return;

    let visible = false;

    // invisible hover zone at top
    const hoverZone = document.createElement("div");
    hoverZone.style.position = "fixed";
    hoverZone.style.top = "0";
    hoverZone.style.left = "0";
    hoverZone.style.width = "100%";
    hoverZone.style.height = "8px";
    hoverZone.style.zIndex = "2000";
    hoverZone.style.background = "transparent";
    document.body.appendChild(hoverZone);

    function showToolbar() {
      if (visible) return;
      toolbar.classList.remove("toolbar-hidden");
      toolbar.style.position = "relative"; // put back into flex flow
      visible = true;
    }

    function hideToolbar() {
      if (!visible) return;
      toolbar.classList.add("toolbar-hidden");
      visible = false;
    }

    // start hidden
    hideToolbar();

    hoverZone.addEventListener("mouseenter", showToolbar);
    toolbar.addEventListener("mouseleave", (ev) => {
      const rt = ev.relatedTarget;
      if (rt && (rt === hoverZone || hoverZone.contains(rt))) return;
      hideToolbar();
    });
  }

  const observer = new MutationObserver((_, obs) => {
    const toolbar = document.querySelector("nav.toolbar.toolbar-header");
    if (toolbar) {
      setupToolbarHider(toolbar);
      obs.disconnect();
    }
  });
  observer.observe(document.body, { childList: true, subtree: true });
})();