GitHub Repositories Shortcut

Add a 'Repositories' tab shortcut to quickly access a user's repositories.

// ==UserScript==
// @name         GitHub Repositories Shortcut
// @description  Add a 'Repositories' tab shortcut to quickly access a user's repositories.
// @icon         https://github.githubassets.com/favicons/favicon-dark.svg
// @version      1.0
// @author       afkarxyz
// @namespace    https://github.com/afkarxyz/userscripts/
// @supportURL   https://github.com/afkarxyz/userscripts/issues
// @license      MIT
// @match        https://github.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const REPO_ICON_SVG_STRING = `<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" data-view-component="true" class="octicon octicon-repo"><path d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 0 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 1 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5Zm10.5-1h-8a1 1 0 0 0-1 1v6.708A2.486 2.486 0 0 1 4.5 9h8ZM5 12.25a.25.25 0 0 1 .25-.25h3.5a.25.25 0 0 1 .25.25v3.25a.25.25 0 0 1-.4.2l-1.45-1.087a.249.249 0 0 0-.3 0L5.4 15.7a.25.25 0 0 1-.4-.2Z"></path></svg>`;
    const DESKTOP_TAB_ID = 'user-repositories-desktop-tab-v16';
    const MOBILE_ICON_ID = 'user-repositories-mobile-icon-v16';
    const DESKTOP_NAV_SELECTOR = '.AppHeader-localBar nav.js-repo-nav ul.UnderlineNav-body';
    const INSIGHTS_TAB_SELECTOR = 'a#insights-tab';
    const MOBILE_BUTTONS_GROUP_SELECTOR = '.d-block.d-md-none .d-flex.gap-2.mt-n3.mb-3.flex-wrap > .d-flex.flex-row.gap-2:first-of-type';
    const MOBILE_STAR_CONTAINER_SELECTOR = '.js-toggler-container.starring-container';
    const CHECK_INTERVAL_MS = 500;
    const MAX_CHECKS_PER_PAGE = 20;
    const GLOBAL_PAGES = ['pulls', 'issues', 'marketplace', 'explore', 'topics', 'sponsors', 'settings', 'notifications', 'new', 'codespaces', 'organizations', 'orgs', 'gist', 'logout', 'login', 'features', 'about', 'pricing', 'security'];

    let currentUrl = location.href;
    let mainIntervalId = null;
    let checksDone = 0;

    function getElement(selector) {
        return document.querySelector(selector);
    }

    function getUsername() {
        const pathParts = window.location.pathname.split('/').filter(Boolean);
        return pathParts[0] || null;
    }

    function isRepoPage() {
        if (!getElement(DESKTOP_NAV_SELECTOR) && !getElement(MOBILE_BUTTONS_GROUP_SELECTOR)) return false;
        const pathParts = window.location.pathname.split('/').filter(Boolean);
        if (pathParts.length < 2 || (pathParts.length === 1 && window.location.search.includes('tab=repositories')) || GLOBAL_PAGES.includes(pathParts[0])) {
            return false;
        }
        return true;
    }

    function removeElements() {
        [DESKTOP_TAB_ID, MOBILE_ICON_ID].forEach(id => {
            const el = document.getElementById(id);
            if (el) (el.closest('li') || el).remove();
        });
    }

    function createLinkElement(id, username, isMobile) {
        const link = document.createElement('a');
        link.id = id;
        link.href = `https://github.com/${username}?tab=repositories`;
        link.dataset.viewComponent = 'true';

        if (isMobile) {
            link.className = 'Button Button--iconOnly Button--secondary Button--medium tooltipped tooltipped-s';
            link.setAttribute('aria-label', `View ${username}'s repositories`);
            link.innerHTML = REPO_ICON_SVG_STRING.replace('<svg ', '<svg class="octicon octicon-repo Button-visual" ');
        } else {
            link.className = 'UnderlineNav-item no-wrap js-responsive-underlinenav-item';
            link.innerHTML = `
                ${REPO_ICON_SVG_STRING.replace('<svg ', '<svg class="octicon octicon-repo UnderlineNav-octicon d-none d-sm-inline" ')}
                <span data-content="Repositories">Repositories</span>
                <span class="Counter" title="User Repositories" data-view-component="true"></span>
            `;
        }
        return link;
    }

    function addDesktopTab(username) {
        if (document.getElementById(DESKTOP_TAB_ID)) return true;
        const navBody = getElement(DESKTOP_NAV_SELECTOR);
        if (!navBody) return false;
        const insightsTabLink = navBody.querySelector(INSIGHTS_TAB_SELECTOR);
        if (!insightsTabLink) return false;
        const insightsTabLi = insightsTabLink.closest('li.d-inline-flex');
        if (!insightsTabLi) return false;

        const newTabLi = document.createElement('li');
        newTabLi.className = 'd-inline-flex';
        newTabLi.dataset.viewComponent = 'true';
        newTabLi.appendChild(createLinkElement(DESKTOP_TAB_ID, username, false));
        insightsTabLi.parentNode.insertBefore(newTabLi, insightsTabLi.nextSibling);
        return true;
    }

    function addMobileIconButton(username) {
        if (document.getElementById(MOBILE_ICON_ID)) return true;
        const buttonsContainer = getElement(MOBILE_BUTTONS_GROUP_SELECTOR);
        if (!buttonsContainer) return false;

        const mobileIconLink = createLinkElement(MOBILE_ICON_ID, username, true);
        const wrapperDiv = document.createElement('div');
        wrapperDiv.className = 'd-inline-block';
        wrapperDiv.appendChild(mobileIconLink);

        const starButtonContainer = buttonsContainer.querySelector(MOBILE_STAR_CONTAINER_SELECTOR) ||
                                    buttonsContainer.querySelector('.BtnGroup + .js-toggler-container');
        if (starButtonContainer && starButtonContainer.parentNode === buttonsContainer) {
            buttonsContainer.insertBefore(wrapperDiv, starButtonContainer.nextSibling);
        } else {
            buttonsContainer.appendChild(wrapperDiv);
        }
        return true;
    }

    function runCheck() {
        checksDone++;
        if (checksDone > MAX_CHECKS_PER_PAGE && mainIntervalId) {
            clearInterval(mainIntervalId);
            mainIntervalId = null;
            return;
        }

        if (isRepoPage()) {
            const username = getUsername();
            if (!username) return;
            const desktopAdded = addDesktopTab(username);
            const mobileAdded = addMobileIconButton(username);

            if ((desktopAdded || mobileAdded || document.getElementById(DESKTOP_TAB_ID) || document.getElementById(MOBILE_ICON_ID)) && currentUrl === location.href && mainIntervalId) {
                 clearInterval(mainIntervalId);
                 mainIntervalId = null;
            }
        } else {
            removeElements();
            if (!mainIntervalId && checksDone <= MAX_CHECKS_PER_PAGE) {
                startInterval();
            }
        }
    }

    function startInterval() {
        if (mainIntervalId) clearInterval(mainIntervalId);
        checksDone = 0;
        mainIntervalId = setInterval(runCheck, CHECK_INTERVAL_MS);
        runCheck();
    }

    function handlePageChange() {
        if (location.href !== currentUrl) {
            currentUrl = location.href;
            removeElements();
            startInterval();
        } else if (!mainIntervalId) {
            startInterval();
        }
    }

    startInterval();
    document.addEventListener('turbo:load', handlePageChange);

    let oldHrefForObserver = document.location.href;
    const bodyObserver = new MutationObserver(() => {
        if (oldHrefForObserver !== document.location.href) {
            oldHrefForObserver = document.location.href;
            handlePageChange();
        }
    });
    const bodyElement = getElement("body");
    if (bodyElement) bodyObserver.observe(bodyElement, { childList: true, subtree: true });

    window.addEventListener('beforeunload', () => {
        if (mainIntervalId) clearInterval(mainIntervalId);
        bodyObserver.disconnect();
    });
})();