GitHub Upload Button for Subfolders

Adds an upload button to GitHub repository subfolder pages, enabling direct file uploads into specific folders.

// ==UserScript==
// @name         GitHub Upload Button for Subfolders
// @description  Adds an upload button to GitHub repository subfolder pages, enabling direct file uploads into specific folders.
// @icon         https://github.githubassets.com/favicons/favicon-dark.svg
// @version      1.1
// @author       afkarxyz
// @namespace    https://github.com/afkarxyz/misc-scripts/
// @supportURL   https://github.com/afkarxyz/misc-scripts/issues
// @license      MIT
// @match        https://github.com/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const style = document.createElement('style');
    style.textContent = `
        .upload-icon {
            display: inline-block;
            cursor: pointer;
            margin-left: 6px;
            vertical-align: text-bottom;
            transition: transform 0.1s ease;
        }
        .upload-icon:hover {
            transform: scale(1.1);
        }
        .commit-age-wrapper {
            display: flex !important;
            align-items: center;
            justify-content: space-between;
            width: 100%;
            min-width: 120px;
            padding-right: 1rem;
        }
        .time-wrapper {
            flex: 0 1 auto;
            min-width: 0;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .icon-wrapper {
            flex: 0 0 auto;
        }
    `;
    document.head.appendChild(style);

    const createUploadIcon = () => {
        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
        svg.setAttribute("viewBox", "0 0 16 16");
        svg.setAttribute("width", "16");
        svg.setAttribute("height", "16");
        svg.setAttribute("fill", "currentColor");
        svg.classList.add('upload-icon');
        svg.setAttribute("aria-label", "Upload to this folder");

        const g = document.createElementNS("http://www.w3.org/2000/svg", "g");

        const path1 = document.createElementNS("http://www.w3.org/2000/svg", "path");
        path1.setAttribute("d", "M7.4,11.4c-0.3,0.3-0.3,0.8,0,1.1c0.3,0.3,0.8,0.3,1.1,0L11,10c0.3-0.3,0.3-0.8,0-1.1L8.5,6.3C8.2,6,7.7,6,7.4,6.3s-0.3,0.8,0,1.1l1.2,1.2l-7.9,0C0.3,8.7,0,9,0,9.4c0,0.4,0.3,0.8,0.8,0.8h7.9L7.4,11.4z");

        const path2 = document.createElementNS("http://www.w3.org/2000/svg", "path");
        path2.setAttribute("d", "M15.5,3.4l-2.9-2.9C12.2,0.2,11.8,0,11.3,0H4.8C3.8,0,3,0.8,3,1.8v5.2h1.5V1.8c0-0.1,0.1-0.2,0.2-0.2H10v2.8c0,1,0.8,1.8,1.8,1.8h2.8v8.2c0,0.1-0.1,0.2-0.2,0.2H4.8c-0.1,0-0.2-0.1-0.2-0.2v-2.3H3v2.3c0,1,0.8,1.8,1.8,1.8h9.5c1,0,1.8-0.8,1.8-1.8V4.7C16,4.2,15.8,3.8,15.5,3.4z M11.8,4.5c-0.1,0-0.2-0.1-0.2-0.2V1.6l0,0l2.9,2.9l0,0H11.8z");

        g.appendChild(path1);
        g.appendChild(path2);
        svg.appendChild(g);

        return svg;
    };

    const getUploadUrl = (row) => {
        try {
            const link = row.querySelector('.react-directory-truncate a');
            if (link) {
                const currentPath = link.getAttribute('title');
                const baseUrl = window.location.pathname.split('/tree/')[0];
                return `https://github.com${baseUrl}/upload/main/${currentPath}`;
            }
        } catch (error) {
            console.error('Error getting upload URL:', error);
        }
        return null;
    };

    const insertUploadIcon = () => {
        try {
            const rows = document.querySelectorAll('.react-directory-row');

            rows.forEach(row => {
                const isFolder = row.querySelector('.icon-directory');

                if (isFolder && !row.querySelector('.upload-icon')) {
                    const commitAgeDiv = row.querySelector('.react-directory-commit-age');
                    if (commitAgeDiv) {
                        const wrapper = document.createElement('div');
                        wrapper.className = 'commit-age-wrapper';
                        
                        const timeWrapper = document.createElement('div');
                        timeWrapper.className = 'time-wrapper';
                        
                        const iconWrapper = document.createElement('div');
                        iconWrapper.className = 'icon-wrapper';

                        const timeElement = commitAgeDiv.querySelector('relative-time');
                        if (timeElement) {
                            timeWrapper.appendChild(timeElement.cloneNode(true));
                            timeElement.remove();
                        }

                        const uploadIcon = createUploadIcon();
                        const uploadUrl = getUploadUrl(row);

                        if (uploadUrl) {
                            uploadIcon.addEventListener('click', (e) => {
                                e.preventDefault();
                                e.stopPropagation();
                                window.location.href = uploadUrl;
                            });

                            iconWrapper.appendChild(uploadIcon);
                        }

                        wrapper.appendChild(timeWrapper);
                        wrapper.appendChild(iconWrapper);
                        commitAgeDiv.appendChild(wrapper);
                    }
                }
            });
        } catch (error) {
            console.error('Error inserting upload icons:', error);
        }
    };

    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if (mutation.addedNodes.length) {
                insertUploadIcon();
            }
        });
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    insertUploadIcon();
})();