Raw Source
xyzzy529 / Gmail Quick Links

// ==UserScript==
// @name         Gmail Quick Links
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Inserts a Gmail Quick Links container above the folders area, sorted alphabetically. The "Add Quick Link" button uses the current browser URL and prompts only for a label. Includes options to export/import quick links using a JSON file. 
// @author       nascent
// @match        https://mail.google.com/*
// @updateURL    https://openuserjs.org/meta/nascent/Gmail_Quick_Links.meta.js
// @downloadURL  https://openuserjs.org/install/nascent/Gmail_Quick_Links.user.js
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// @copyright    2025, nascent (https://openuserjs.org/users/nascent)
// @license      MIT
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
  "use strict";

  /************************************************************************
   * Inject Custom CSS
   *
   * This CSS block scales down the quick links container (relative sizing)
   * and supplies fallback icons for empty glyph spans.
   ************************************************************************/
  if (!document.getElementById("custom-gmail-quicklinks-style")) {
    const styleEl = document.createElement("style");
    styleEl.id = "custom-gmail-quicklinks-style";
    styleEl.textContent = `
      /* Scale down the quick links container */
      #gmailQuickLinksContainer {
          font-size: 0.8em !important;
      }
      /* Fallback icons if Gmail’s native glyphs aren’t applied */
      #gmailQuickLinksContainer .glyph.global::before {
          content: "🌐";
      }
      #gmailQuickLinksContainer .glyph.rename::before {
          content: "✎";
          font-weight: bold;
      }
      #gmailQuickLinksContainer .glyph.delete::before {
          content: "🗑";
      }
      /* Clear the float */
      #gmailQuickLinksContainer .clear {
          clear: both;
      }
    `;
    document.head.appendChild(styleEl);
  }

  /************************************************************************
   * Trusted Types Setup
   *
   * Gmail enforces Trusted Types so wrap HTML strings via a policy.
   ************************************************************************/
  const trustedPolicy = window.trustedTypes ?
    window.trustedTypes.createPolicy("myPolicy", {
      createHTML: input => input
    }) :
    null;

  /************************************************************************
   * Default Quick Links
   *
   * Only the two Inbox links are provided as defaults.
   ************************************************************************/
  const defaultQuickLinks = [{
      label: "Inbox",
      href: "#advanced-search/is_unread=true&query=is:inbox+-category:promotions+-is:starred",
      title: "Inbox"
    },
    {
      label: "Inbox (No Categories)",
      href: "#advanced-search/is_unread=true&query=is:inbox+-category:promotions+-category:social+-category:updates+-category:forums+-is:starred",
      title: "Inbox (No Categories)"
    }
  ];

  /************************************************************************
   * Utility Functions: Load, Save, and Sort Quick Links
   ************************************************************************/
  function loadQuickLinks() {
    const stored = GM_getValue("quickLinks", null);
    if (stored) {
      try {
        return JSON.parse(stored);
      }
      catch (e) {
        console.error("Failed to parse quickLinks from storage", e);
      }
    }
    return defaultQuickLinks;
  }

  function sortQuickLinks(links) {
    return links.sort((a, b) =>
      a.label.toLowerCase().localeCompare(b.label.toLowerCase())
    );
  }

  function saveQuickLinks(links) {
    GM_setValue("quickLinks", JSON.stringify(links));
  }

  /************************************************************************
   * Render Quick Links List
   *
   * Builds the display for each quick link.
   ************************************************************************/
  function renderQuickLinks() {
    const listContainer = document.getElementById("listContainer");
    if (!listContainer) return;

    const initialHTML = '<div style="padding-left: 30px;"></div>';
    listContainer.innerHTML = trustedPolicy ?
      trustedPolicy.createHTML(initialHTML) :
      initialHTML;

    const innerContainer = listContainer.firstElementChild;
    const links = loadQuickLinks();

    links.forEach((link, index) => {
      const linkDiv = document.createElement("div");

      // Create the anchor element.
      const a = document.createElement("a");
      a.className = "n0";
      a.href = link.href;
      a.title = link.title || link.label;
      a.style.textDecoration = "underline";
      a.textContent = link.label;
      linkDiv.appendChild(a);

      // Global icon
      // I never used this so not implementing - global by default
      /*const globalSpan = document.createElement("span");
      globalSpan.className = "glyph global";
      globalSpan.title = "toggle global/single";
      globalSpan.style.cursor = "pointer";
      globalSpan.style.marginLeft = "5px";
      linkDiv.appendChild(globalSpan);*/

      // Rename icon
      const renameSpan = document.createElement("span");
      renameSpan.className = "glyph rename";
      renameSpan.title = "rename";
      renameSpan.style.cursor = "pointer";
      renameSpan.style.marginLeft = "5px";
      renameSpan.addEventListener("click", (e) => {
        e.stopPropagation();
        e.preventDefault();
        const newLabel = prompt("Enter new label", link.label);
        if (newLabel) {
          let current = loadQuickLinks();
          current[index].label = newLabel;
          current[index].title = newLabel;
          // Sort links alphabetically before saving.
          current = sortQuickLinks(current);
          saveQuickLinks(current);
          renderQuickLinks();
        }
      });
      linkDiv.appendChild(renameSpan);

      // Delete icon
      const deleteSpan = document.createElement("span");
      deleteSpan.className = "glyph delete";
      deleteSpan.title = "delete";
      deleteSpan.style.cursor = "pointer";
      deleteSpan.style.marginLeft = "5px";
      deleteSpan.addEventListener("click", (e) => {
        e.stopPropagation();
        e.preventDefault();
        if (confirm("Delete this quick link?")) {
          let current = loadQuickLinks();
          current.splice(index, 1);
          // Sort links alphabetically before saving.
          current = sortQuickLinks(current);
          saveQuickLinks(current);
          renderQuickLinks();
        }
      });
      linkDiv.appendChild(deleteSpan);

      // Clear div for layout.
      const clearDiv = document.createElement("div");
      clearDiv.className = "clear";
      linkDiv.appendChild(clearDiv);

      innerContainer.appendChild(linkDiv);
    });
  }

  /************************************************************************
   * Insert Quick Links Container
   *
   * Inserts the Quick Links container (header and list) into Gmail’s
   * navigation area as the first child so it appears above the folders/labels.
   ************************************************************************/
  function insertQuickLinksContainer() {
    const nav = document.querySelector('[role="navigation"]');
    if (nav && !document.getElementById("gmailQuickLinksContainer")) {
      const html = `
<div id="gmailQuickLinksContainer">
  <div data-reactroot="" id="gmailQuickLinks" class="">
    <div>
      <div class="r" style="display: flex; align-items: baseline; justify-content: space-between;">
        <div style="cursor: auto; position: relative; overflow: hidden; vertical-align: middle; outline: none; font-size: 100%;">
          <span class="glyph info" title="info/help"></span>
          <h2 class="pw">Quick Links</h2>
        </div>
        <div class="n0" id="addQuickLinkButton" title="Add Quick Link" style="text-decoration: underline; cursor:pointer;">
          Add Quick Link
        </div>
      </div>
      <div id="listContainer" style="padding-bottom: 10px;">
        <div style="padding-left: 30px;"></div>
      </div>
    </div>
  </div>
</div>
      `;
      const safeHTML = trustedPolicy ? trustedPolicy.createHTML(html) : html;
      const fragment = document.createRange().createContextualFragment(safeHTML);
      nav.insertBefore(fragment, nav.firstChild);

      // Set up the "Add Quick Link" button.
      const addButton = document.getElementById("addQuickLinkButton");
      if (addButton) {
        addButton.addEventListener("click", () => {
          const label = prompt("Enter Quick Link Label:");
          if (!label) return;
          // Automatically use the current browser location for the URL.
          const href = window.location.href;
          let current = loadQuickLinks();
          current.push({
            label: label,
            href: href,
            title: label
          });
          // Sort links alphabetically before saving.
          current = sortQuickLinks(current);
          saveQuickLinks(current);
          renderQuickLinks();
        });
      }

      renderQuickLinks();
      console.log("Custom Gmail Quick Links container inserted.");
    }
  }

  /************************************************************************
   * Export and Import Functions
   *
   * Export: Converts the quick links to JSON and triggers a download as a file.
   * Import: Opens a file chooser and, upon file selection, reads and parses the
   *         JSON to overwrite existing quick links.
   ************************************************************************/
  function exportQuickLinks() {
    const links = loadQuickLinks();
    const json = JSON.stringify(links, null, 2);
    const blob = new Blob([json], {
      type: "application/json"
    });
    const url = URL.createObjectURL(blob);
    if (typeof GM_download === "function") {
      // Use GM_download if available.
      GM_download({
        url: url,
        name: "quicklinks.json"
      });
    }
    else {
      const a = document.createElement("a");
      a.href = url;
      a.download = "quicklinks.json";
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
    }
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  function importQuickLinks() {
    const input = document.createElement("input");
    input.type = "file";
    input.accept = "application/json";
    input.style.display = "none";
    input.addEventListener("change", function (event) {
      const file = event.target.files[0];
      if (!file) return;
      const reader = new FileReader();
      reader.onload = function (e) {
        try {
          const data = JSON.parse(e.target.result);
          if (confirm("This will overwrite your existing quick links. Continue?")) {
            saveQuickLinks(data);
            renderQuickLinks();
            alert("Quick links imported successfully.");
          }
        }
        catch (err) {
          alert("Error importing quick links: " + err);
        }
      };
      reader.readAsText(file);
    });
    document.body.appendChild(input);
    input.click();
    input.remove();
  }

  /************************************************************************
   * Observe Gmail's Dynamic DOM
   *
   * Gmail’s UI is dynamic, so a MutationObserver reinserts container when needed.
   ************************************************************************/
  const observer = new MutationObserver(() => {
    insertQuickLinksContainer();
  });
  observer.observe(document.body, {
    childList: true,
    subtree: true
  });

  // Insert the container immediately.
  insertQuickLinksContainer();

  /************************************************************************
   * Tampermonkey Menu Commands
   *
   * Registers two menu items:
   *   Export Quicklinks – triggers a file download of your quick links as JSON.
   *   Import Quicklinks – allows you to select a JSON file to overwrite your quick links.
   ************************************************************************/
  if (typeof GM_registerMenuCommand !== "undefined") {
    GM_registerMenuCommand("Export Quicklinks", exportQuickLinks);
    GM_registerMenuCommand("Import Quicklinks", importQuickLinks);
  }
})();