您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
为 GitHub 仓库添加本地备注
当前为
- // ==UserScript==
- // @name GitHub Repo Notes
- // @name:zh-CN GitHub 仓库备注工具
- // @namespace http://tampermonkey.net/
- // @version 1.3
- // @description Add local notes to GitHub repository
- // @description:zh-CN 为 GitHub 仓库添加本地备注
- // @author Ivans
- // @match https://github.com/*
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_deleteValue
- // @grant GM_listValues
- // @icon https://cdn.simpleicons.org/github/808080
- // @license MIT
- // @supportURL https://github.com/Ivans-11/github-repo-notes/issues
- // ==/UserScript==
- (function() {
- 'use strict';
- const NOTE_KEY_PREFIX = 'gh_repo_note_';
- // Get the full name of the repository
- function getRepoFullName(card) {
- const link = card.querySelector('h3 a[itemprop="name codeRepository"]') ||
- card.querySelector('h3 a') ||
- card.querySelector('.search-title a') ||
- card.querySelector('a.Link--primary.Link.text-bold[data-hovercard-type="repository"]');
- if (!link) return null;
- const href = link.getAttribute('href');
- if (!href) return null;
- return href.substring(1);
- }
- // Get the note from local storage
- function getNote(repoFullName) {
- // Convert to lowercase
- return GM_getValue(NOTE_KEY_PREFIX + repoFullName.toLowerCase(), '');
- }
- // Set the note to local storage
- function setNote(repoFullName, note) {
- if (note) {
- GM_setValue(NOTE_KEY_PREFIX + repoFullName.toLowerCase(), note);
- } else {
- GM_deleteValue(NOTE_KEY_PREFIX + repoFullName.toLowerCase());
- }
- }
- // Create the note button
- function createNoteButton(repoFullName, note, onClick) {
- const btn = document.createElement('button');
- const icon = document.createElement('span');
- icon.innerHTML = `<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" style="vertical-align: text-bottom; margin-right: 8px; fill: var(--fgColor-muted,var(--color-fg-muted));">
- <path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Z"></path>
- </svg>`;
- btn.appendChild(icon);
- btn.appendChild(document.createTextNode(note ? 'Edit' : 'Add'));
- btn.style.margin = '4px';
- btn.style.borderRadius = '6px';
- btn.style.padding = '2px 8px';
- btn.style.fontSize = '12px';
- btn.style.cursor = 'pointer';
- btn.style.fontFamily = 'var(--fontStack-sansSerif)';
- btn.style.lineHeight = '20px';
- btn.style.fontWeight = '600';
- btn.style.color = 'var(--button-default-fgColor-rest, var(--color-btn-text))';
- btn.style.backgroundColor = 'var(--button-default-bgColor-rest, var(--color-btn-bg))';
- btn.style.border = '1px solid var(--button-default-borderColor-rest,var(--color-btn-border))';
- btn.style.display = 'flex';
- btn.style.alignItems = 'center';
- btn.style.height = 'var(--control-small-size,1.75rem)';
- btn.addEventListener('click', onClick);
- return btn;
- }
- // Create the note display
- function createNoteDisplay(note) {
- const div = document.createElement('div');
- const icon = document.createElement('span');
- icon.innerHTML = `<svg aria-hidden="true" height="16" viewBox="0 0 16 16" version="1.1" width="16" style="vertical-align: text-bottom; margin-right: 8px; fill: var(--fgColor-muted,var(--color-fg-muted));">
- <path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Z"></path>
- </svg>`;
- div.appendChild(icon);
- div.appendChild(document.createTextNode(note));
- div.style.borderRadius = '6px';
- div.style.padding = '4px 8px';
- div.style.marginTop = '2px';
- div.style.marginBottom = '6px';
- div.style.fontSize = '13px';
- div.style.fontFamily = 'var(--fontStack-sansSerif)';
- div.style.lineHeight = '20px';
- div.style.display = 'flex';
- div.style.alignItems = 'center';
- return div;
- }
- // Prompt the user to input the note
- function promptNote(oldNote) {
- let note = prompt('Please input your notes (leave blank to be deleted):', oldNote || '');
- if (note === null) return undefined;
- note = note.trim();
- return note;
- }
- // Insert the button and note on the card
- function enhanceCard(card) {
- if (card.dataset.noteEnhanced) return; // Avoid duplicate
- const repoFullName = getRepoFullName(card);
- if (!repoFullName) return;
- card.dataset.noteEnhanced = '1';
- // Find the star button
- let starBtn = card.querySelector('.js-toggler-container.js-social-container.starring-container, .Box-sc-g0xbh4-0.fvaNTI');
- if (!starBtn) return;
- // Create the button
- let note = getNote(repoFullName);
- let btn = createNoteButton(repoFullName, note, function() {
- let newNote = promptNote(note);
- if (typeof newNote === 'undefined') return;
- setNote(repoFullName, newNote);
- // Re-render
- card.dataset.noteEnhanced = '';
- enhanceCard(card);
- });
- // Insert the button
- if (starBtn.parentElement) {
- // Avoid duplicate insertion
- let oldBtn = starBtn.parentElement.querySelector('.gh-note-btn');
- if (oldBtn) oldBtn.remove();
- btn.classList.add('gh-note-btn');
- starBtn.parentElement.appendChild(btn);
- }
- // Note display
- let oldNoteDiv = card.querySelector('.gh-note-display');
- if (oldNoteDiv) oldNoteDiv.remove();
- if (note) {
- let noteDiv = createNoteDisplay(note);
- noteDiv.classList.add('gh-note-display');
- // Put it before the data bar
- let dataInfo = card.querySelector('.f6.color-fg-muted.mt-0.mb-0.width-full, .f6.color-fg-muted.mt-2, .Box-sc-g0xbh4-0.bZkODq');
- if (dataInfo && dataInfo.parentElement) {
- dataInfo.parentElement.insertBefore(noteDiv, dataInfo);
- }
- }
- }
- // Select all repository cards
- function findAllRepoCards() {
- // Adapt to multiple card structures
- let cards = Array.from(document.querySelectorAll(`
- .col-12.d-block.width-full.py-4.border-bottom.color-border-muted,
- li.col-12.d-flex.flex-justify-between.width-full.py-4.border-bottom.color-border-muted,
- .Box-sc-g0xbh4-0.iwUbcA,
- .Box-sc-g0xbh4-0.flszRz,
- .Box-sc-g0xbh4-0.jbaXRR,
- .Box-sc-g0xbh4-0.bmHqGc,
- .Box-sc-g0xbh4-0.hFxojJ,
- section[aria-label="card content"]
- `));
- // Filter out cards without a repository full name
- return cards.filter(card => getRepoFullName(card));
- }
- // Determine if it is a repository page
- function isRepoPage() {
- const path = window.location.pathname;
- const parts = path.split('/').filter(Boolean);
- return (parts.length === 2 || (parts.length === 4 && parts[2] === 'tree')) &&
- !path.includes('/blob/') &&
- !path.includes('/issues/') &&
- !path.includes('/pulls/');
- }
- // Repository page processing function
- function enhanceRepoPage() {
- if (document.body.dataset.noteEnhanced) return; // Avoid duplicate
- // Get the repository name from the link
- const path = window.location.pathname;
- const parts = path.split('/').filter(Boolean);
- const repoFullName = parts.slice(0, 2).join('/');
- if (!repoFullName) return;
- document.body.dataset.noteEnhanced = '1';
- // Create the first button
- let note = getNote(repoFullName);
- let btn = createNoteButton(repoFullName, note, function() {
- let newNote = promptNote(note);
- if (typeof newNote === 'undefined') return;
- setNote(repoFullName, newNote);
- // Re-render
- document.body.dataset.noteEnhanced = '';
- enhanceRepoPage();
- });
- // Find the button bar
- let actionsList = document.querySelector('.pagehead-actions');
- if (actionsList) {
- // Create a new li element
- let li = document.createElement('li');
- li.appendChild(btn);
- // Avoid duplicate insertion
- let oldLi = actionsList.querySelector('.gh-note-li');
- if (oldLi) oldLi.remove();
- li.classList.add('gh-note-li');
- // Add to the button bar
- actionsList.appendChild(li);
- }
- // Create the second button
- let btn2 = createNoteButton(repoFullName, note, function() {
- let newNote = promptNote(note);
- if (typeof newNote === 'undefined') return;
- setNote(repoFullName, newNote);
- // Re-render
- document.body.dataset.noteEnhanced = '';
- enhanceRepoPage();
- });
- // Find the container
- let container = document.querySelector('.container-xl:not(.d-flex):not(.clearfix)');
- if (container) {
- // Find the star button
- let starBtn = container.querySelector('div[data-view-component="true"].js-toggler-container.starring-container');
- if (starBtn && starBtn.parentElement) {
- // Avoid duplicate insertion
- let oldBtn = starBtn.parentElement.querySelector('.gh-note-btn');
- if (oldBtn) oldBtn.remove();
- let newBtn = btn2;
- newBtn.classList.add('gh-note-btn');
- starBtn.parentElement.appendChild(newBtn);
- }
- }
- // Remove the old note display
- let oldNoteDiv = document.querySelector('.gh-note-display');
- if (oldNoteDiv) oldNoteDiv.remove();
- let oldNoteDiv2 = document.querySelector('.gh-note-display2');
- if (oldNoteDiv2) oldNoteDiv2.remove();
- if (note) {
- // Create the first note display
- let noteDiv = createNoteDisplay(note);
- noteDiv.classList.add('gh-note-display');
- // Find the description
- let description = document.querySelector('.f4.my-3, .f4.my-3.color-fg-muted.text-italic');
- if (description && description.parentElement) {
- description.parentElement.insertBefore(noteDiv, description.nextSibling);
- }
- // Create the second note display
- let noteDiv2 = createNoteDisplay(note);
- noteDiv2.classList.add('gh-note-display2');
- if (container) {
- // Try finding the description
- let newDescription = container.querySelector('p.f4.mb-3.color-fg-muted');
- if (newDescription && newDescription.parentElement) {
- newDescription.parentElement.insertBefore(noteDiv2, newDescription.nextSibling);
- } else {
- // If there is no description, find the flex container
- let flexContainer = container.querySelector('div.d-flex.gap-2.mt-n3.mb-3.flex-wrap');
- if (flexContainer && flexContainer.parentElement) {
- flexContainer.parentElement.insertBefore(noteDiv2, flexContainer.nextSibling);
- }
- }
- }
- }
- }
- // Export notes data
- function exportNotes() {
- const notes = {};
- const keys = GM_listValues();
- for (let i = 0; i < keys.length; i++) {
- const key = keys[i];
- if (key.startsWith(NOTE_KEY_PREFIX)) {
- notes[key] = GM_getValue(key);
- }
- }
- const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(notes));
- const downloadAnchorNode = document.createElement('a');
- downloadAnchorNode.setAttribute("href", dataStr);
- downloadAnchorNode.setAttribute("download", "github_repo_notes.json");
- document.body.appendChild(downloadAnchorNode);
- downloadAnchorNode.click();
- downloadAnchorNode.remove();
- }
- // Import notes data
- function importNotes() {
- const input = document.createElement('input');
- input.type = 'file';
- input.accept = '.json';
- input.onchange = (e) => {
- const file = e.target.files[0];
- if (file) {
- const reader = new FileReader();
- reader.onload = (event) => {
- try {
- const notes = JSON.parse(event.target.result);
- for (const key in notes) {
- if (notes.hasOwnProperty(key) && key.startsWith(NOTE_KEY_PREFIX)) {
- GM_setValue(key, notes[key]);
- }
- }
- alert('Import successfully!');
- enhanceAll();
- } catch (error) {
- alert('Error:' + error.message);
- }
- };
- reader.readAsText(file);
- }
- };
- input.click();
- }
- // Clear all notes data
- function clearNotes() {
- const keys = GM_listValues();
- for (let i = 0; i < keys.length; i++) {
- const key = keys[i];
- if (key.startsWith(NOTE_KEY_PREFIX)) {
- GM_deleteValue(key);
- }
- }
- }
- // Create the floating button
- function createFloatingButton(text, onClick) {
- const btn = document.createElement('button');
- btn.textContent = text;
- btn.style.margin = '5px';
- btn.style.borderRadius = '6px';
- btn.style.backgroundColor = 'var(--button-default-bgColor-rest, var(--color-btn-bg))';
- btn.style.color = 'var(--button-default-fgColor-rest, var(--color-btn-text))';
- btn.style.border = '1px solid var(--button-default-borderColor-rest,var(--color-btn-border))';
- btn.addEventListener('click', onClick);
- return btn;
- }
- // Create the bottom floating button
- function createBottomButton() {
- if (document.querySelector('.gh-import-export-btn')) return; // Avoid duplicate
- const bottomBtn = document.createElement('button');
- bottomBtn.textContent = '☰';
- bottomBtn.classList.add('gh-import-export-btn');
- bottomBtn.style.position = 'fixed';
- bottomBtn.style.bottom = '20px';
- bottomBtn.style.right = '20px';
- bottomBtn.style.zIndex = '1000';
- bottomBtn.style.padding = '10px 20px';
- bottomBtn.style.backgroundColor = 'var(--button-default-bgColor-rest, var(--color-btn-bg))';
- bottomBtn.style.color = 'var(--button-default-fgColor-rest, var(--color-btn-text))';
- bottomBtn.style.border = '1px solid var(--button-default-borderColor-rest,var(--color-btn-border))';
- bottomBtn.style.borderRadius = '50%';
- bottomBtn.style.fontSize = '14px';
- bottomBtn.style.cursor = 'pointer';
- bottomBtn.addEventListener('click', () => {
- if (document.querySelector('.gh-import-export-dialog')) {
- document.querySelector('.gh-import-export-dialog').remove();
- bottomBtn.textContent = '☰';
- bottomBtn.style.borderRadius = '50%';
- return;
- }
- // Expand the button
- bottomBtn.textContent = 'Import/Export notes data';
- bottomBtn.style.borderRadius = '8px';
- // Create the dialog
- const dialog = document.createElement('div');
- dialog.classList.add('gh-import-export-dialog');
- dialog.style.position = 'fixed';
- dialog.style.bottom = '60px';
- dialog.style.right = '20px';
- dialog.style.backgroundColor = 'rgba(255, 255, 255, 0)';
- dialog.style.border = 'none';
- dialog.style.padding = '10px';
- dialog.style.zIndex = '1001';
- const exportBtn = createFloatingButton('Export', () => {
- exportNotes();
- dialog.remove();
- });
- const importBtn = createFloatingButton('Import', () => {
- importNotes();
- dialog.remove();
- });
- const clearBtn = createFloatingButton('Clear', () => {
- if (!confirm('Are you sure you want to clear all notes?')) return;
- clearNotes();
- dialog.remove();
- });
- dialog.appendChild(exportBtn);
- dialog.appendChild(importBtn);
- dialog.appendChild(clearBtn);
- document.body.appendChild(dialog);
- });
- document.body.appendChild(bottomBtn);
- }
- // Initial processing
- function enhanceAll() {
- if (isRepoPage()) {
- enhanceRepoPage();
- } else {
- findAllRepoCards().forEach(enhanceCard);
- }
- createBottomButton();
- }
- // Listen for DOM changes to adapt to dynamic loading
- const observer = new MutationObserver(() => {
- enhanceAll();
- });
- observer.observe(document.body, {childList: true, subtree: true});
- // First load
- enhanceAll();
- })();