TGithub

Adds some features on github.com & gitlab.com to integrate it with tecnativa.com

Verze ze dne 27. 01. 2026. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name           TGithub
// @author         Alexandre Díaz, Carlos Roca, Carlos Dauden
// @version        1.17.2
// @grant          none
// @run-at         document-idle
// @namespace      tecnativa
// @icon           https://www.tecnativa.com/web/image/website/1/favicon/
// @match          *://github.com/*
// @match          *://gitlab.tecnativa.com/*
// @description    Adds some features on github.com & gitlab.com to integrate it with tecnativa.com
// ==/UserScript==

(function (window) {
    "use strict";

    const TGithub = {
        ODOO_SERVER: 'https://www.tecnativa.com',
        COMPANY_NAME: 'Tecnativa',
        REGEX_TEMPLATES: {},

        init: function () {
            if (this.initialized) {
                return;
            }
            this.initialized = true;
            this._addRegexTemplate('TT', /\bTT(\d+)/gi, `<a target='_blank' href='${this.ODOO_SERVER}/web#id=$1&model=project.task&view_type=form'>${this.COMPANY_NAME}-Task #$1</a>`);
            this._replaceTask();
            if (this._isLocationHost('github')) {
                this._ensureGhNavbarButton();
                this._startGhNavbarKeepAlive();
            }
        },

        _isLocationHost: function (host) {
            return document.location.host.toLowerCase().includes(host);
        },

        _addRegexTemplate: function (templateName, regex, html) {
            this.REGEX_TEMPLATES[templateName] = { regex, html };
        },

        _executeRegexReplace: function (templateName, text) {
            if (templateName in this.REGEX_TEMPLATES && text.match(this.REGEX_TEMPLATES[templateName].regex)) {
                return text.replace(this.REGEX_TEMPLATES[templateName].regex, this.REGEX_TEMPLATES[templateName].html);
            }
            return false;
        },

        _replaceTask: function () {
            const searchAndParse = () => {
                document.querySelectorAll('.comment-body, .note-text, .description, .commit-description, .js-comment-body, .js-issue-title, .markdown-body').forEach((elm) => {
                    const htmlTemplate = this._executeRegexReplace('TT', elm.innerHTML);
                    if (htmlTemplate) {
                        elm.innerHTML = htmlTemplate;
                    }
                });
            };
            // Setting up MutationObserver to handle dynamic content updates
            if (typeof this.observer === 'undefined') {
                const targetNode = document.body;

                if (targetNode) {
                    this.observer = new MutationObserver((mutations) => {
                        mutations.forEach(() => searchAndParse());
                    });
                    this.observer.observe(targetNode, { childList: true, subtree: true });
                }
            }
            // Initial execution to replace already loaded content
            searchAndParse();
        },

        _findGhHeaderTarget: function () {
            return (
                document.querySelector('.AppHeader-globalBar-end') ||
                document.querySelector('div[data-testid="top-bar-actions"]')
            );
        },

        _ensureGhNavbarButton: function () {
            const targetNode = this._findGhHeaderTarget();
            if (!targetNode) return false;

            const existing = targetNode.querySelector('#pulls-tecnativa');
            if (existing && existing.isConnected) return true;
            document.querySelectorAll('#pulls-tecnativa').forEach((n) => n.remove());

            const menuItem = document.createElement('a');
            menuItem.id = 'pulls-tecnativa';
            menuItem.textContent = this.COMPANY_NAME;
            menuItem.href = `/pulls?q=is%3Aopen+is%3Apr+archived%3Afalse+involves%3A${this.COMPANY_NAME}`;

            const exampleItem =
                targetNode.querySelector("a[href$='/pulls']") ||
                targetNode.querySelector("a[href*='/pulls']") ||
                targetNode.querySelector('a');

            if (exampleItem) {
                menuItem.className = exampleItem.className;
                menuItem.style.cssText = (exampleItem.style.cssText || '') + 'width:auto;';
            } else {
                menuItem.style.cssText = 'margin-right:8px; display:inline-flex; align-items:center;';
            }

            targetNode.insertAdjacentElement('afterbegin', menuItem);
            return true;
        },

        _startGhNavbarKeepAlive: function () {
            if (this._keepAliveStarted) return;
            this._keepAliveStarted = true;

            const tick = () => this._ensureGhNavbarButton();
            tick();
            if (!this._navbarObserver) {
                this._navbarObserver = new MutationObserver(() => {
                    const target = this._findGhHeaderTarget();
                    if (target && !target.querySelector('#pulls-tecnativa')) {
                        this._ensureGhNavbarButton();
                    }
                });
                this._navbarObserver.observe(document.body, { childList: true, subtree: true });
            }
            if (!this._navbarInterval) {
                this._navbarInterval = setInterval(() => {
                    const target = this._findGhHeaderTarget();
                    if (!target) return;
                    if (!target.querySelector('#pulls-tecnativa')) this._ensureGhNavbarButton();
                }, 2000);
            }
            const burst = () => {
                let i = 0;
                const run = () => {
                    i += 1;
                    tick();
                    if (i < 10) requestAnimationFrame(run);
                };
                requestAnimationFrame(run);
            };

            window.addEventListener('popstate', burst, true);
            this._onNavBurst = burst;
        },

        // Override history methods to detect navigation
        overrideHistoryMethods: function () {
            const originalPushState = history.pushState;
            const originalReplaceState = history.replaceState;
            const handleStateChange = () => {
                this.initialized = false; // Allow reinitialization
                this.init();
            };
            history.pushState = (...args) => {
                originalPushState.apply(history, args);
                handleStateChange();
            };
            history.replaceState = (...args) => {
                originalReplaceState.apply(history, args);
                handleStateChange();
            };
            window.addEventListener('popstate', handleStateChange);
        },
    };

    // Initialize the script when the DOM is ready
    const ready = (callback) => {
        if (document.readyState !== 'loading') {
            callback();
        } else {
            document.addEventListener('DOMContentLoaded', callback);
        }
    };

    ready(() => {
        TGithub.init();
        TGithub.overrideHistoryMethods();
    });

})(window);