Add GitHub Releases Tab

Add Releases tab after Code tab on GitHub repos

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         Add GitHub Releases Tab
// @namespace    https://github.com/nvbangg/nvbangg-projects
// @version      1.7
// @description  Add Releases tab after Code tab on GitHub repos
// @author       nvbangg (https://github.com/nvbangg)
// @copyright    Copyright (c) 2026 nvbangg (github.com/nvbangg)
// @match        https://github.com/*/*
// @icon         https://github.com/favicon.ico
// @license      MIT
// @run-at       document-start
// ==/UserScript==

(() => {
  "use strict";

  const TAB_ID = "releases-tab-link";

  // Returns true when the current page is the releases section.
  function isReleasesPage() {
    return /\/releases(\/|$)/.test(location.pathname);
  }

  function getCodeAnchor() {
    return document.querySelector(
      'nav[aria-label="Repository"] a[data-tab-item="code"], nav[aria-label="Repository"] #code-tab'
    );
  }

  // Sets or removes aria-current="page" on our tab to match GitHub's
  // own active-tab convention (the only visual difference between tabs).
  // When Releases is active, also strip aria-current from the Code tab so
  // only one tab appears highlighted at a time.
  function updateActiveState() {
    const anchor     = document.getElementById(TAB_ID);
    const codeAnchor = getCodeAnchor();
    if (!anchor) return;

    if (isReleasesPage()) {
      anchor.setAttribute("aria-current", "page");
      // Code tab must not appear active while we're on the releases page.
      if (codeAnchor) codeAnchor.removeAttribute("aria-current");
    } else {
      anchor.removeAttribute("aria-current");
      // Let GitHub's own rendering decide whether Code is active again;
      // we only restore it if it was previously cleared by us and GitHub
      // hasn't already re-set it via Turbo.
      if (codeAnchor && !codeAnchor.hasAttribute("aria-current")) {
        const onCodePage = /^\/[^/]+\/[^/]+(\/tree\/|\/blob\/|\/)?$/.test(location.pathname);
        if (onCodePage) codeAnchor.setAttribute("aria-current", "page");
      }
    }
  }

  function insertReleasesTab() {
    const nav = document.querySelector('nav[aria-label="Repository"]');
    if (!nav || nav.querySelector(`#${TAB_ID}`)) {
      // Tab already exists — just sync the active state.
      updateActiveState();
      return;
    }

    // Bail if releases tab already exists natively.
    if (nav.querySelector('a[href$="/releases"]')) return;

    const codeAnchor = nav.querySelector('a[data-tab-item="code"], #code-tab');
    if (!codeAnchor) return;

    const urlParts = location.pathname.split("/");
    if (urlParts.length < 3) return;
    const releasesHref = `/${urlParts[1]}/${urlParts[2]}/releases`;

    // Read class names live from the Code tab so we match whatever
    // GitHub is currently using — no hardcoded strings that can go stale.
    const liClass = codeAnchor.closest("li")?.className ?? "";
    const aClass  = codeAnchor.className ?? "";

    // Build the SVG using the same attributes as every other tab icon.
    const existingSvg = codeAnchor.querySelector("svg");
    const svgAttrs = existingSvg
      ? `data-component="Octicon" aria-hidden="true" focusable="false" class="octicon octicon-tag" viewBox="0 0 16 16" width="${existingSvg.getAttribute("width") || 16}" height="${existingSvg.getAttribute("height") || 16}" fill="currentColor" display="inline-block" overflow="visible" style="vertical-align: text-bottom;"`
      : `aria-hidden="true" focusable="false" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"`;

    // aria-current="page" is set here immediately if we are already on the
    // releases page (e.g. hard reload directly onto a releases URL).
    const ariaCurrent = isReleasesPage() ? 'aria-current="page"' : "";

    const html = `
      <li class="${liClass}">
        <a id="${TAB_ID}"
           href="${releasesHref}"
           class="${aClass}"
           ${ariaCurrent}
           data-tab-item="releases"
           data-turbo-frame="repo-content-turbo-frame"
           data-discover="true"
           aria-label="Releases">
          <span data-component="icon">
            <svg ${svgAttrs}>
              <path d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"></path>
            </svg>
          </span>
          <span data-component="text" data-content="Releases">Releases</span>
        </a>
      </li>`;

    const codeLi = codeAnchor.closest("li") ?? codeAnchor;
    codeLi.insertAdjacentHTML("afterend", html);

    // Guard against GitHub's overflow system hiding our tab.
    const injectedLi     = document.getElementById(TAB_ID)?.closest("li");
    const injectedAnchor = document.getElementById(TAB_ID);
    if (!injectedLi || !injectedAnchor) return;

    new MutationObserver(() => {
      if (injectedLi.getAttribute("aria-hidden") === "true")
        injectedLi.removeAttribute("aria-hidden");
      if (injectedAnchor.getAttribute("tabindex") === "-1")
        injectedAnchor.removeAttribute("tabindex");
    }).observe(injectedLi, { attributes: true });
  }

  // ── SPA navigation ────────────────────────────────────────────────────────
  // insertReleasesTab re-inserts the tab after Turbo swaps the nav out,
  // and also calls updateActiveState() when the tab already exists.
  // popstate covers back/forward without a Turbo event.
  document.addEventListener("turbo:load",      insertReleasesTab);
  document.addEventListener("turbolinks:load", insertReleasesTab);
  window.addEventListener("popstate",          insertReleasesTab);

  insertReleasesTab();
  new MutationObserver(insertReleasesTab).observe(document.documentElement, {
    childList: true,
    subtree: true,
  });
})();