GreenWall: View all contribution graphs in GitHub ⬜🟩

View a graph of users' contributions over the years in GitHub.

// ==UserScript==
// @name               GreenWall: View all contribution graphs in GitHub ⬜🟩
// @description        View a graph of users' contributions over the years in GitHub.
// @name:zh-CN         GreenWall - 查看历年 GitHub 的贡献图 ⬜🟩
// @description:zh-CN  在 GitHub 中查看用户历年的贡献图。
// @version            1.2.0
// @namespace          https://green-wall.leoku.dev
// @author             LeoKu(https://leoku.dev)
// @match              https://github.com/*
// @run-at             document-start
// @icon               https://green-wall.leoku.dev/favicon.svg
// @grant              GM.xmlHttpRequest
// @homepageURL        https://github.com/Codennnn/Green-Wall
// @license            MIT
// ==/UserScript==

var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
    if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
        if (ar || !(i in from)) {
            if (!ar) ar = Array.prototype.slice.call(from, 0, i);
            ar[i] = from[i];
        }
    }
    return to.concat(ar || Array.prototype.slice.call(from));
};
function addHistoryEvent(type) {
    var originalMethod = window.history[type];
    return function () {
        var args = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
        }
        originalMethod.apply(window.history, args);
        var ev = new Event(type);
        window.dispatchEvent(ev);
    };
}
window.history.replaceState = addHistoryEvent('replaceState');
var handler = function () {
    var _a, _b, _c;
    var githubUserPageRegex = /^https:\/\/github\.com\/[a-zA-Z0-9-]+(?=\/?$)/;
    var isProfile = githubUserPageRegex.test(window.location.href);
    if (isProfile) {
        var ORIGIN_1 = 'https://green-wall.leoku.dev';
        var produceData_1 = function (_a) {
            var data = _a.data;
            var contributionCalendars = data.contributionCalendars.map(function (cur) {
                var rows = [[], [], [], [], [], [], []];
                var nullDay = { count: 0, date: '', level: 'Null' };
                cur.weeks.forEach(function (_a) {
                    var days = _a.days;
                    if (days.length !== 7) {
                        var newDays = __spreadArray([], days, true);
                        for (var i = 0; i <= 6; i++) {
                            var theDay = newDays.at(i);
                            var weekday = i;
                            if (theDay && typeof theDay.weekday === 'number') {
                                if (theDay.weekday === weekday) {
                                    rows[theDay.weekday].push(theDay);
                                }
                                else {
                                    newDays.splice(i, 0, nullDay);
                                    rows[i].push(nullDay);
                                }
                            }
                            else {
                                rows[i].push(nullDay);
                            }
                        }
                    }
                    else {
                        days.forEach(function (day) {
                            if (typeof day.weekday === 'number') {
                                rows[day.weekday].push(day);
                            }
                        });
                    }
                });
                var calendar = {
                    total: cur.total,
                    year: cur.year,
                    rows: rows,
                };
                return calendar;
            });
            return {
                contributionCalendars: contributionCalendars,
            };
        };
        var isHalloween_1 = false;
        var createGraph_1 = function (params) {
            var year = params.year, total = params.total, rows = params.rows;
            var table = document.createElement('table');
            table.classList.add('ContributionCalendar-grid');
            table.style.borderSpacing = '3px';
            table.style.overflow = 'hidden';
            table.style.position = 'relative';
            var tbody = document.createElement('tbody');
            var tr = document.createElement('tr');
            tr.style.height = '10px';
            rows.forEach(function (row) {
                var clonedTr = tr.cloneNode();
                var htmlStr = '';
                row.forEach(function (col, idx) {
                    var td = '<td></td>';
                    if (col.level !== "Null" /* ContributionLevel.Null */) {
                        var level = col.level === "NONE" /* ContributionLevel.NONE */
                            ? 0
                            : col.level === "FIRST_QUARTILE" /* ContributionLevel.FIRST_QUARTILE */
                                ? 1
                                : col.level === "SECOND_QUARTILE" /* ContributionLevel.SECOND_QUARTILE */
                                    ? 2
                                    : col.level === "THIRD_QUARTILE" /* ContributionLevel.THIRD_QUARTILE */
                                        ? 3
                                        : 4;
                        td = "\n            <td\n              title=\"".concat(col.count === 0 ? 'No' : col.count, " contributions in ").concat(col.date, "\"\n              tabindex=\"-1\"\n              data-ix=\"").concat(idx, "\"\n              style=\"width: 10px\"\n              data-level=\"").concat(level, "\"\n              class=\"ContributionCalendar-day\"\n              data-date=\"").concat(col.level, "\"\n              aria-selected=\"false\"\n              role=\"gridcell\"\n            ></td>\n            ");
                    }
                    htmlStr += td;
                });
                if (clonedTr instanceof HTMLTableRowElement) {
                    clonedTr.innerHTML = htmlStr;
                    tbody.append(clonedTr);
                }
            });
            table.appendChild(tbody);
            var graphItem = document.createElement('div');
            var countText = document.createElement('div');
            countText.style.marginBottom = '5px';
            countText.textContent = "".concat(total, " contributions in ").concat(year);
            graphItem.append(countText, table);
            if (isHalloween_1) {
                graphItem.classList.add('ContributionCalendar');
                graphItem.setAttribute('data-holiday', 'halloween');
            }
            return { graphItem: graphItem };
        };
        var createDialog = function (params) {
            var username = params.username;
            var dialog = document.createElement('dialog');
            dialog.id = 'green-wall-dialog';
            dialog.classList.add('Overlay', 'Overlay-whenNarrow', 'Overlay--size-medium-portrait', 'Overlay--motion-scaleFadeOverlay', 'Overlay-whenNarrow', 'Overlay--size-medium-portrait', 'Overlay--motion-scaleFade');
            dialog.style.minWidth = '720px';
            dialog.style.maxHeight = 'calc(100vh - 50px)';
            dialog.addEventListener('close', function () {
                document.body.classList.remove('has-modal');
            });
            var mouseDownTarget;
            var mouseDownHandler = function (ev) {
                if (ev.target instanceof HTMLElement) {
                    mouseDownTarget = ev.target;
                }
            };
            var mouseUpHandler = function (ev) {
                if (ev.target instanceof HTMLDialogElement &&
                    ev.target === mouseDownTarget &&
                    ev.target === dialog) {
                    dialog.close();
                }
            };
            dialog.addEventListener('mousedown', mouseDownHandler);
            dialog.addEventListener('mouseup', mouseUpHandler);
            // ---
            var wrap = document.createElement('div');
            wrap.style.display = 'flex';
            wrap.style.flexDirection = 'column';
            wrap.style.overflow = 'hidden';
            // ---
            var dialogHeader = document.createElement('div');
            dialogHeader.classList.add('Overlay-header');
            var contentWrap = document.createElement('div');
            contentWrap.classList.add('Overlay-headerContentWrap');
            var titleWrap = document.createElement('div');
            titleWrap.classList.add('Overlay-titleWrap');
            var title = document.createElement('h1');
            title.classList.add('Overlay-title');
            title.textContent = "".concat(username, "'s GreenWall");
            var actionWrap = document.createElement('div');
            actionWrap.classList.add('Overlay-actionWrap');
            var actionButton = document.createElement('button');
            actionButton.classList.add('close-button', 'Overlay-closeButton');
            actionButton.setAttribute('type', 'button');
            actionButton.innerHTML = "\n      <svg aria-hidden=\"true\" height=\"16\" viewBox=\"0 0 16 16\" version=\"1.1\" width=\"16\" data-view-component=\"true\" class=\"octicon octicon-x\">\n        <path d=\"M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z\"></path>\n      </svg>\n      ";
            actionButton.addEventListener('click', function (ev) {
                ev.stopPropagation();
                dialog.close();
            });
            // ---
            var dialogBody = document.createElement('div');
            dialogBody.classList.add('Overlay-body');
            dialogBody.style.overflowY = 'auto';
            var dialogContent = document.createElement('div');
            dialogContent.style.display = 'flex';
            dialogContent.style.flexDirection = 'column';
            dialogContent.style.rowGap = '10px';
            dialogContent.style.alignItems = 'center';
            dialogContent.style.padding = 'var(--stack-padding-normal, 1rem)';
            // ---
            var dialogFooter = document.createElement('div');
            dialogFooter.classList.add('Overlay-footer', 'Overlay-footer--alignEnd', 'Overlay-footer--divided');
            var openExtrnalBtn = document.createElement('button');
            var btnContent = document.createElement('span');
            btnContent.classList.add('Button-label');
            btnContent.textContent = 'Open in Green Wall';
            openExtrnalBtn.classList.add('Button', 'Button--primary', 'Button--medium');
            openExtrnalBtn.addEventListener('click', function () {
                window.open("".concat(ORIGIN_1, "/user/").concat(username), '_blank');
            });
            titleWrap.append(title);
            actionWrap.append(actionButton);
            contentWrap.append(titleWrap, actionWrap);
            openExtrnalBtn.append(btnContent);
            dialogHeader.append(contentWrap);
            dialogBody.append(dialogContent);
            dialogFooter.append(openExtrnalBtn);
            wrap.append(dialogHeader, dialogBody, dialogFooter);
            dialog.append(wrap);
            document.body.append(dialog);
            return { dialog: dialog, dialogContent: dialogContent };
        };
        var profileArea = document.querySelector('.Layout-sidebar .h-card .js-profile-editable-replace');
        var refNode = (_b = (_a = document.querySelector('.js-profile-editable-replace > .d-flex.flex-column')) === null || _a === void 0 ? void 0 : _a.nextSibling) === null || _b === void 0 ? void 0 : _b.nextSibling;
        if (profileArea instanceof HTMLElement && refNode instanceof HTMLElement) {
            var username_1 = (_c = document
                .querySelector('meta[name="octolytics-dimension-user_login"]')) === null || _c === void 0 ? void 0 : _c.getAttribute('content');
            if (username_1) {
                var exists = !!document.querySelector('#green-wall-block');
                if (!exists) {
                    var block = document.createElement('div');
                    block.setAttribute('id', 'green-wall-block');
                    block.classList.add('border-top', 'color-border-muted', 'pt-3', 'mt-3', 'clearfix', 'hide-sm', 'hide-md');
                    var title = document.createElement('h2');
                    title.classList.add('h4', 'mb-2');
                    title.textContent = 'Green Wall';
                    var openBtn = document.createElement('button');
                    openBtn.classList.add('btn');
                    openBtn.textContent = ' ⬜🟩 View All Green';
                    block.appendChild(title);
                    block.appendChild(openBtn);
                    profileArea.insertBefore(block, refNode);
                    var _d = createDialog({ username: username_1 }), dialog_1 = _d.dialog, dialogContent_1 = _d.dialogContent;
                    var hasLoaded_1 = false;
                    var handleLoadError_1 = function () {
                        dialogContent_1.innerHTML = '';
                        var errorBlock = document.createElement('div');
                        errorBlock.style.display = 'flex';
                        errorBlock.style.flexDirection = 'column';
                        errorBlock.style.alignItems = 'center';
                        var tip = document.createElement('p');
                        tip.textContent = 'The process of obtaining data has an exception.';
                        var retryBtn = document.createElement('button');
                        retryBtn.classList.add('btn');
                        retryBtn.textContent = 'Retry';
                        retryBtn.addEventListener('click', function () {
                            // eslint-disable-next-line @typescript-eslint/no-use-before-define
                            handleLoadData_1();
                        });
                        errorBlock.append(tip, retryBtn);
                        dialogContent_1.append(errorBlock);
                    };
                    var handleLoadData_1 = function () {
                        var loading = "\n            <svg aria-label=\"Loading\" style=\"box-sizing: content-box; color: var(--color-icon-primary);\" width=\"32\" height=\"32\" viewBox=\"0 0 16 16\" fill=\"none\" data-view-component=\"true\" class=\"anim-rotate\">\n              <circle cx=\"8\" cy=\"8\" r=\"7\" stroke=\"currentColor\" stroke-opacity=\"0.25\" stroke-width=\"2\" vector-effect=\"non-scaling-stroke\" fill=\"none\"></circle>\n              <path d=\"M15 8a7.002 7.002 0 00-7-7\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" vector-effect=\"non-scaling-stroke\"></path>\n            </svg>\n            ";
                        dialogContent_1.innerHTML = loading;
                        GM.xmlHttpRequest({
                            method: 'GET',
                            url: "".concat(ORIGIN_1, "/api/contribution/").concat(username_1),
                            onload: function (response) {
                                try {
                                    dialogContent_1.innerHTML = '';
                                    var data = JSON.parse(response.responseText);
                                    var xData = produceData_1(data);
                                    xData.contributionCalendars.forEach(function (calendar) {
                                        var graphItem = createGraph_1(calendar).graphItem;
                                        dialogContent_1.append(graphItem);
                                    });
                                    hasLoaded_1 = true;
                                }
                                catch (_a) {
                                    handleLoadError_1();
                                }
                            },
                            onerror: function (err) {
                                console.error('[Green Wall]: ', err);
                                handleLoadError_1();
                            },
                        });
                    };
                    var handleDialogOpen_1 = function () {
                        var _a;
                        dialog_1.showModal();
                        document.body.classList.add('has-modal');
                        if (!hasLoaded_1) {
                            isHalloween_1 =
                                ((_a = document.querySelector('.ContributionCalendar')) === null || _a === void 0 ? void 0 : _a.getAttribute('data-holiday')) ===
                                    'halloween';
                            handleLoadData_1();
                        }
                    };
                    openBtn.addEventListener('click', function () {
                        handleDialogOpen_1();
                    });
                }
            }
        }
        else {
            console.warn('[Green Wall]: Target node not found.');
        }
    }
};
window.addEventListener('replaceState', handler);