RepoNotes

RepoNotes is a lightweight browser extension script for Tampermonkey that enhances your GitHub stars with personalized notes. Ever starred a repository but later forgot why? RepoNotes solves this problem by allowing you to attach custom annotations to your starred repositories.

目前為 2025-03-30 提交的版本,檢視 最新版本

// ==UserScript==
// @name         RepoNotes
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  RepoNotes is a lightweight browser extension script for Tampermonkey that enhances your GitHub stars with personalized notes. Ever starred a repository but later forgot why? RepoNotes solves this problem by allowing you to attach custom annotations to your starred repositories.
// @author       malagebidi
// @match        https://github.com/*?tab=stars*
// @icon         https://github.githubassets.com/favicons/favicon.svg
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(async function() {
    'use strict';

    // --- Configuration ---
    const NOTE_PLACEHOLDER = 'Enter your note...';
    const ADD_BUTTON_TEXT = 'Add Note';
    const EDIT_BUTTON_TEXT = 'Edit Note';
    const SAVE_BUTTON_TEXT = 'Save';
    const CANCEL_BUTTON_TEXT = 'Cancel';
    const DELETE_BUTTON_TEXT = 'Delete';

    // --- Styles ---
    GM_addStyle(`
        .ghsn-container {
            padding-right: var(--base-size-24, 24px) !important;
            color: var(--fgColor-muted, var(--color-fg-muted)) !important;
            width: 74.99999997%;
        }
        .ghsn-display {
            font-style: italic;
            border: var(--borderWidth-thin) solid var(--borderColor-default, var(--color-border-default, #d2dff0));
            border-radius: 100px;
            padding: 2.5px 5px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            display: block;
            max-width: fit-content;
        }
        .ghsn-textarea {
            width: 100%;
            min-height: 60px;
            margin-bottom: 5px;
            padding: 5px;
            border: 1px solid var(--color-border-default);
            border-radius: 3px;
            background-color: var(--color-canvas-default);
            color: var(--color-fg-default);
            box-sizing: border-box;
        }
        .ghsn-buttons button {
            margin-right: 5px;
            padding: 3px 8px;
            font-size: 0.9em;
            cursor: pointer;
            border-radius: 4px;
            border: 1px solid var(--color-border-muted);
        }
        .ghsn-buttons button.ghsn-save {
            background-color: var(--color-btn-primary-bg);
            color: var(--color-btn-primary-text);
            border-color: var(--color-btn-primary-border);
        }
        .ghsn-buttons button.ghsn-delete {
            background-color: var(--color-btn-danger-bg);
            color: var(--color-btn-danger-text);
            border-color: var(--color-btn-danger-border);
        }
        .ghsn-buttons button.ghsn-cancel {
            background-color: var(--color-btn-bg);
            color: var(--color-btn-text);
        }
        .ghsn-buttons button:hover {
            filter: brightness(1.1);
        }
        .ghsn-hidden {
            display: none !important;
        }
        .ghsn-note-btn {
            margin-left: 16px;
            color: var(--fgColor-muted);
            cursor: pointer;
            text-decoration: none;
        }
        .ghsn-note-btn:hover {
            color: var(--fgColor-accent) !important;
            -webkit-text-decoration: none;
            text-decoration: none;
        }
        .ghsn-note-btn svg {
            margin-right: 4px;
        }
    `);

    // --- Core Logic ---

    // Get repo unique identifier (owner/repo)
    function getRepoFullName(repoElement) {
        const link = repoElement.querySelector('h3 a, h2 a');
        if (link && link.pathname) {
            return link.pathname.substring(1); // Remove leading '/'
        }
        return null;
    }

    // Create note button with icon
    function createNoteButton(isEdit = false) {
        const button = document.createElement('a');
        button.className = 'ghsn-note-btn';
        button.href = 'javascript:void(0);';

        // SVG icon (notebook)
        const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        svg.setAttribute('aria-hidden', 'true');
        svg.setAttribute('height', '16');
        svg.setAttribute('width', '16');
        svg.setAttribute('viewBox', '0 0 16 16');
        svg.setAttribute('fill', 'currentColor');
        svg.setAttribute('class', 'octicon octicon-star');

        // Notebook icon
        const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
        path.setAttribute('d', 'M11.013 1.427a1.75 1.75 0 0 1 2.474 0l1.086 1.086a1.75 1.75 0 0 1 0 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 0 1-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61Zm.176 4.823L9.75 4.81l-6.286 6.287a.253.253 0 0 0-.064.108l-.558 1.953 1.953-.558a.253.253 0 0 0 .108-.064Zm1.238-3.763a.25.25 0 0 0-.354 0L10.811 3.75l1.439 1.44 1.263-1.263a.25.25 0 0 0 0-.354Z');

        svg.appendChild(path);
        button.appendChild(svg);

        const text = document.createTextNode(isEdit ? EDIT_BUTTON_TEXT : ADD_BUTTON_TEXT);
        button.appendChild(text);

        return button;
    }

    // Add note UI for a single repository
    async function addNoteUI(repoElement) {
        // Prevent duplicate
        if (repoElement.querySelector('.ghsn-note-btn')) {
            return;
        }

        const repoFullName = getRepoFullName(repoElement);
        if (!repoFullName) {
            console.warn('Could not find repo name for element:', repoElement);
            return;
        }

        const storageKey = `ghsn_${repoFullName}`;
        let currentNote = await GM_getValue(storageKey, '');

        // Find the star count element to place our button next to it
        const starCountElement = repoElement.querySelector('.Link--muted.mr-3');
        if (!starCountElement) {
            console.warn('Could not find star count element for repo:', repoFullName);
            return;
        }

        // Create Add/Edit Note button
        const noteButton = createNoteButton(currentNote ? true : false);

        // Find the parent row that contains the star count
        const starRow = starCountElement.parentNode;

        // Insert button at the end of the row that contains the star count
        starRow.appendChild(noteButton);

        // Create note container (initially hidden if no note)
        const container = document.createElement('div');
        container.className = 'ghsn-container';
        if (!currentNote) {
            container.classList.add('ghsn-hidden');
        }

        const displaySpan = document.createElement('span');
        displaySpan.className = 'ghsn-display';
        displaySpan.textContent = currentNote;

        const noteTextarea = document.createElement('textarea');
        noteTextarea.className = 'ghsn-textarea ghsn-hidden';
        noteTextarea.placeholder = NOTE_PLACEHOLDER;

        const buttonsDiv = document.createElement('div');
        buttonsDiv.className = 'ghsn-buttons';

        const saveButton = document.createElement('button');
        saveButton.textContent = SAVE_BUTTON_TEXT;
        saveButton.className = 'ghsn-save ghsn-hidden';

        const cancelButton = document.createElement('button');
        cancelButton.textContent = CANCEL_BUTTON_TEXT;
        cancelButton.className = 'ghsn-cancel ghsn-hidden';

        const deleteButton = document.createElement('button');
        deleteButton.textContent = DELETE_BUTTON_TEXT;
        deleteButton.className = 'ghsn-delete ghsn-hidden';

        // --- Event Listeners ---

        // Add/Edit Note button click
        noteButton.addEventListener('click', () => {
            if (container.classList.contains('ghsn-hidden')) {
                container.classList.remove('ghsn-hidden');
            }

            noteTextarea.value = currentNote;
            displaySpan.classList.add('ghsn-hidden');

            noteTextarea.classList.remove('ghsn-hidden');
            saveButton.classList.remove('ghsn-hidden');
            cancelButton.classList.remove('ghsn-hidden');

            // Only show delete button in edit mode if there's an existing note
            if (currentNote) {
                deleteButton.classList.remove('ghsn-hidden');
            }

            noteTextarea.focus();
        });

        // Cancel button click
        cancelButton.addEventListener('click', () => {
            noteTextarea.classList.add('ghsn-hidden');
            deleteButton.classList.add('ghsn-hidden');
            saveButton.classList.add('ghsn-hidden');
            cancelButton.classList.add('ghsn-hidden');

            // If there's no note and user cancels, hide the container
            if (!currentNote) {
                container.classList.add('ghsn-hidden');
            } else {
                displaySpan.classList.remove('ghsn-hidden');
            }
        });

        // Save button click
        saveButton.addEventListener('click', async () => {
            const newNote = noteTextarea.value.trim();
            await GM_setValue(storageKey, newNote);
            currentNote = newNote;

            // Update button text based on whether there's a note
            noteButton.textContent = newNote ? EDIT_BUTTON_TEXT : ADD_BUTTON_TEXT;

            // Prepend the icon to the button
            const icon = createNoteButton().firstChild;
            noteButton.insertBefore(icon, noteButton.firstChild);

            if (newNote) {
                displaySpan.textContent = newNote;
                displaySpan.classList.remove('ghsn-hidden');
            } else {
                // If note is deleted/empty, hide the container
                container.classList.add('ghsn-hidden');
            }

            noteTextarea.classList.add('ghsn-hidden');
            deleteButton.classList.add('ghsn-hidden');
            saveButton.classList.add('ghsn-hidden');
            cancelButton.classList.add('ghsn-hidden');
        });

        // Delete button click
        deleteButton.addEventListener('click', async () => {
            if (confirm(`Are you sure you want to delete the note for "${repoFullName}"?`)) {
                await GM_deleteValue(storageKey);
                currentNote = '';

                // Update button
                noteButton.textContent = ADD_BUTTON_TEXT;
                const icon = createNoteButton().firstChild;
                noteButton.insertBefore(icon, noteButton.firstChild);

                // Hide container
                container.classList.add('ghsn-hidden');

                // Hide all edit elements
                noteTextarea.classList.add('ghsn-hidden');
                deleteButton.classList.add('ghsn-hidden');
                saveButton.classList.add('ghsn-hidden');
                cancelButton.classList.add('ghsn-hidden');
            }
        });

        // --- Assemble UI ---
        buttonsDiv.appendChild(deleteButton);
        buttonsDiv.appendChild(saveButton);
        buttonsDiv.appendChild(cancelButton);

        container.appendChild(displaySpan);
        container.appendChild(noteTextarea);
        container.appendChild(buttonsDiv);

        // --- Insert into page ---
        const description = repoElement.querySelector('p.col-9');
        const topics = repoElement.querySelector('.topic-tag-list');
        const insertAfterElement = topics || description || repoElement.querySelector('h3, h2');

        if (insertAfterElement && insertAfterElement.parentNode) {
            insertAfterElement.parentNode.insertBefore(container, insertAfterElement.nextSibling);
        } else {
            repoElement.appendChild(container);
        }
    }

    // --- Process all repositories on the page ---
    function processRepositories() {
        const repoElements = document.querySelectorAll('div.col-12.d-block.width-full.py-4');
        repoElements.forEach(addNoteUI);
    }

    // --- Observe DOM changes (handle dynamic loading) ---
    const observer = new MutationObserver(mutations => {
        mutations.forEach(mutation => {
            mutation.addedNodes.forEach(node => {
                if (node.nodeType === 1) {
                    if (node.matches('div.col-12.d-block.width-full.py-4')) {
                        addNoteUI(node);
                    } else {
                        const nestedRepos = node.querySelectorAll('div.col-12.d-block.width-full.py-4');
                        nestedRepos.forEach(addNoteUI);
                    }
                }
            });
        });
    });

    // --- Startup ---
    processRepositories();

    const targetNode = document.querySelector('div.position-relative') || document.querySelector('main') || document.body;
    observer.observe(targetNode, { childList: true, subtree: true });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址