您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhances GitHub commits with beautiful labels for conventional commit types (feat, fix, docs, etc.)
当前为
// ==UserScript== // @name GitHub Commit Labels // @namespace https://github.com/nazdridoy // @version 1.2.1 // @description Enhances GitHub commits with beautiful labels for conventional commit types (feat, fix, docs, etc.) // @author nazdridoy // @license MIT // @match https://github.com/* // @icon https://github.githubassets.com/favicons/favicon.svg // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-end // @homepageURL https://github.com/nazdridoy/github-commit-labels // @supportURL https://github.com/nazdridoy/github-commit-labels/issues // ==/UserScript== /* MIT License Copyright (c) 2025 nazDridoy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function() { 'use strict'; // Detect GitHub theme (dark, light, or dark dimmed) function detectTheme() { const html = document.documentElement; const colorMode = html.getAttribute('data-color-mode'); // Handle sync with system (auto) setting if (colorMode === 'auto') { // Get the system preference const darkThemeMedia = window.matchMedia('(prefers-color-scheme: dark)'); const isDarkMode = darkThemeMedia.matches; if (isDarkMode) { // System is in dark mode, but we need to check what's set for "Night theme" const darkThemeSetting = html.getAttribute('data-dark-theme'); // If a light theme variant is set for "Night theme" if (darkThemeSetting && darkThemeSetting.startsWith('light')) { return darkThemeSetting; // Return the specific light theme variant } // Otherwise return the dark theme variant return darkThemeSetting === 'dark_dimmed' ? 'dark_dimmed' : 'dark'; } else { // System is in light mode, check what's set for "Day theme" const lightThemeSetting = html.getAttribute('data-light-theme'); // If a dark theme variant is set for "Day theme" if (lightThemeSetting && lightThemeSetting.startsWith('dark')) { return lightThemeSetting; // Return the specific dark theme variant } return 'light'; // Default to light theme } } // Direct theme setting (not auto) if (colorMode === 'dark') { const darkTheme = html.getAttribute('data-dark-theme'); return darkTheme === 'dark_dimmed' ? 'dark_dimmed' : 'dark'; } else { const lightTheme = html.getAttribute('data-light-theme'); // If a specific light theme variant is set if (lightTheme && lightTheme !== 'light') { return lightTheme; } return 'light'; } } // Helper function to determine if a theme is a dark variant function isDarkTheme(theme) { return theme && (theme === 'dark' || theme === 'dark_dimmed' || theme === 'dark_high_contrast' || theme === 'dark_colorblind' || theme === 'dark_tritanopia'); } // Get current theme let currentTheme = detectTheme(); // Watch for system color scheme changes const darkThemeMedia = window.matchMedia('(prefers-color-scheme: dark)'); darkThemeMedia.addEventListener('change', () => { if (document.documentElement.getAttribute('data-color-mode') === 'auto') { updateThemeColors(); } }); // Color definitions based on theme const THEME_COLORS = { light: { 'green': { bg: 'rgba(35, 134, 54, 0.1)', text: '#1a7f37' }, 'purple': { bg: 'rgba(163, 113, 247, 0.1)', text: '#8250df' }, 'blue': { bg: 'rgba(47, 129, 247, 0.1)', text: '#0969da' }, 'light-blue': { bg: 'rgba(31, 111, 235, 0.1)', text: '#0550ae' }, 'yellow': { bg: 'rgba(210, 153, 34, 0.1)', text: '#9e6a03' }, 'orange': { bg: 'rgba(219, 109, 40, 0.1)', text: '#bc4c00' }, 'gray': { bg: 'rgba(139, 148, 158, 0.1)', text: '#57606a' }, 'light-green': { bg: 'rgba(57, 211, 83, 0.1)', text: '#1a7f37' }, 'red': { bg: 'rgba(248, 81, 73, 0.1)', text: '#cf222e' }, 'dark-yellow': { bg: 'rgba(187, 128, 9, 0.1)', text: '#9e6a03' } }, dark: { 'green': { bg: 'rgba(35, 134, 54, 0.2)', text: '#7ee787' }, 'purple': { bg: 'rgba(163, 113, 247, 0.2)', text: '#d2a8ff' }, 'blue': { bg: 'rgba(47, 129, 247, 0.2)', text: '#79c0ff' }, 'light-blue': { bg: 'rgba(31, 111, 235, 0.2)', text: '#58a6ff' }, 'yellow': { bg: 'rgba(210, 153, 34, 0.2)', text: '#e3b341' }, 'orange': { bg: 'rgba(219, 109, 40, 0.2)', text: '#ffa657' }, 'gray': { bg: 'rgba(139, 148, 158, 0.2)', text: '#8b949e' }, 'light-green': { bg: 'rgba(57, 211, 83, 0.2)', text: '#56d364' }, 'red': { bg: 'rgba(248, 81, 73, 0.2)', text: '#ff7b72' }, 'dark-yellow': { bg: 'rgba(187, 128, 9, 0.2)', text: '#bb8009' } }, dark_dimmed: { 'green': { bg: 'rgba(35, 134, 54, 0.15)', text: '#6bc46d' }, 'purple': { bg: 'rgba(163, 113, 247, 0.15)', text: '#c297ff' }, 'blue': { bg: 'rgba(47, 129, 247, 0.15)', text: '#6cb6ff' }, 'light-blue': { bg: 'rgba(31, 111, 235, 0.15)', text: '#539bf5' }, 'yellow': { bg: 'rgba(210, 153, 34, 0.15)', text: '#daaa3f' }, 'orange': { bg: 'rgba(219, 109, 40, 0.15)', text: '#f0883e' }, 'gray': { bg: 'rgba(139, 148, 158, 0.15)', text: '#909dab' }, 'light-green': { bg: 'rgba(57, 211, 83, 0.15)', text: '#6bc46d' }, 'red': { bg: 'rgba(248, 81, 73, 0.15)', text: '#e5534b' }, 'dark-yellow': { bg: 'rgba(187, 128, 9, 0.15)', text: '#daaa3f' } } }; // Get colors for current theme let COLORS = THEME_COLORS[currentTheme]; // Define default configuration const DEFAULT_CONFIG = { removePrefix: true, enableTooltips: true, labelsVisible: true, labelStyle: { fontSize: '14px', fontWeight: '500', height: '24px', padding: '0 10px', marginRight: '8px', borderRadius: '20px', minWidth: 'auto', textAlign: 'center', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', whiteSpace: 'nowrap', background: 'rgba(0, 0, 0, 0.2)', backdropFilter: 'blur(4px)', border: '1px solid rgba(240, 246, 252, 0.1)', // Subtle border color: '#ffffff' }, commitTypes: { // Features feat: { emoji: '✨', label: 'Feature', color: 'green', description: 'New user features (not for new files without user features)' }, feature: { emoji: '✨', label: 'Feature', color: 'green', description: 'New user features (not for new files without user features)' }, // Added added: { emoji: '📝', label: 'Added', color: 'green', description: 'New files/resources with no user-facing features' }, add: { emoji: '📝', label: 'Added', color: 'green', description: 'New files/resources with no user-facing features' }, // Updated update: { emoji: '♻️', label: 'Updated', color: 'blue', description: 'Changes to existing functionality' }, updated: { emoji: '♻️', label: 'Updated', color: 'blue', description: 'Changes to existing functionality' }, // Removed removed: { emoji: '🗑️', label: 'Removed', color: 'red', description: 'Removing files/code' }, remove: { emoji: '🗑️', label: 'Removed', color: 'red', description: 'Removing files/code' }, delete: { emoji: '🗑️', label: 'Removed', color: 'red', description: 'Removing files/code' }, del: { emoji: '🗑️', label: 'Removed', color: 'red', description: 'Removing files/code' }, // Fixes fix: { emoji: '🐛', label: 'Fix', color: 'purple', description: 'Bug fixes/corrections to errors' }, bugfix: { emoji: '🐛', label: 'Fix', color: 'purple', description: 'Bug fixes/corrections to errors' }, fixed: { emoji: '🐛', label: 'Fix', color: 'purple', description: 'Bug fixes/corrections to errors' }, hotfix: { emoji: '🚨', label: 'Hot Fix', color: 'red', description: 'Critical bug fixes requiring immediate attention' }, // Documentation docs: { emoji: '📚', label: 'Docs', color: 'blue', description: 'Documentation only changes' }, doc: { emoji: '📚', label: 'Docs', color: 'blue', description: 'Documentation only changes' }, documentation: { emoji: '📚', label: 'Docs', color: 'blue', description: 'Documentation only changes' }, // Styling style: { emoji: '💎', label: 'Style', color: 'light-green', description: 'Formatting/whitespace changes (no code change)' }, ui: { emoji: '🎨', label: 'UI', color: 'light-green', description: 'User interface changes' }, css: { emoji: '💎', label: 'Style', color: 'light-green', description: 'CSS/styling changes' }, // Code Changes refactor: { emoji: '📦', label: 'Refactor', color: 'light-blue', description: 'Restructured code (no behavior change)' }, perf: { emoji: '🚀', label: 'Performance', color: 'purple', description: 'Performance improvements' }, performance: { emoji: '🚀', label: 'Performance', color: 'purple', description: 'Performance improvements' }, optimize: { emoji: '⚡', label: 'Optimize', color: 'purple', description: 'Code optimization without functional changes' }, // Testing test: { emoji: '🧪', label: 'Test', color: 'yellow', description: 'Test-related changes' }, tests: { emoji: '🧪', label: 'Test', color: 'yellow', description: 'Test-related changes' }, testing: { emoji: '🧪', label: 'Test', color: 'yellow', description: 'Test-related changes' }, // Build & Deploy build: { emoji: '🛠', label: 'Build', color: 'orange', description: 'Build system changes' }, ci: { emoji: '⚙️', label: 'CI', color: 'gray', description: 'CI pipeline changes' }, cd: { emoji: '🚀', label: 'CD', color: 'gray', description: 'Continuous deployment changes' }, deploy: { emoji: '📦', label: 'Deploy', color: 'orange', description: 'Deployment related changes' }, release: { emoji: '🚀', label: 'Deploy', color: 'orange', description: 'Production releases' }, // Maintenance chore: { emoji: '♻️', label: 'Chore', color: 'light-green', description: 'Routine maintenance tasks' }, deps: { emoji: '📦', label: 'Dependencies', color: 'light-green', description: 'Dependency updates or changes' }, dep: { emoji: '📦', label: 'Dependencies', color: 'light-green', description: 'Dependency updates or changes' }, dependencies: { emoji: '📦', label: 'Dependencies', color: 'light-green', description: 'Dependency updates or changes' }, revert: { emoji: '🗑', label: 'Revert', color: 'red', description: 'Reverting previous changes' }, wip: { emoji: '🚧', label: 'WIP', color: 'dark-yellow', description: 'Work in progress' }, // Security security: { emoji: '🔒', label: 'Security', color: 'red', description: 'Security-related changes' }, // Internationalization i18n: { emoji: '🌐', label: 'i18n', color: 'blue', description: 'Internationalization and localization' }, // Accessibility a11y: { emoji: '♿', label: 'Accessibility', color: 'purple', description: 'Accessibility improvements' }, // API changes api: { emoji: '🔌', label: 'API', color: 'light-blue', description: 'API-related changes' }, // Database changes data: { emoji: '🗃️', label: 'Database', color: 'orange', description: 'Database schema or data changes' }, // Configuration changes config: { emoji: '⚙️', label: 'Config', color: 'gray', description: 'Configuration changes' }, // Initial setup init: { emoji: '🎬', label: 'Init', color: 'green', description: 'Initial commit/project setup' } } }; // Get saved configuration or use default const USER_CONFIG = GM_getValue('commitLabelsConfig', DEFAULT_CONFIG); // Ensure backward compatibility with older versions if (USER_CONFIG.enableTooltips === undefined) { USER_CONFIG.enableTooltips = true; GM_setValue('commitLabelsConfig', USER_CONFIG); } // Ensure labelsVisible exists in config (for backward compatibility) if (USER_CONFIG.labelsVisible === undefined) { USER_CONFIG.labelsVisible = true; GM_setValue('commitLabelsConfig', USER_CONFIG); } // Make sure all commit types have descriptions (for backward compatibility) let configUpdated = false; Object.entries(USER_CONFIG.commitTypes).forEach(([type, config]) => { if (!config.description && DEFAULT_CONFIG.commitTypes[type]) { USER_CONFIG.commitTypes[type].description = DEFAULT_CONFIG.commitTypes[type].description; configUpdated = true; } }); if (configUpdated) { GM_setValue('commitLabelsConfig', USER_CONFIG); } // Create floating toggle button for labels function createLabelToggle() { // Only create if we're on a commit page if (!isCommitPage()) return; // Check if toggle already exists if (document.getElementById('commit-labels-toggle')) return; const toggleBtn = document.createElement('button'); toggleBtn.id = 'commit-labels-toggle'; toggleBtn.textContent = USER_CONFIG.labelsVisible ? '🏷️' : '🏷️'; toggleBtn.title = USER_CONFIG.labelsVisible ? 'Hide commit labels' : 'Show commit labels'; toggleBtn.style.cssText = ` position: fixed; bottom: 20px; right: 20px; width: 32px; height: 32px; border-radius: 6px; background: rgba(31, 35, 40, 0.6); color: #adbac7; border: 1px solid rgba(205, 217, 229, 0.1); font-size: 14px; cursor: pointer; z-index: 9999; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 5px rgba(0,0,0,0.2); opacity: 0.5; transition: opacity 0.2s, transform 0.2s, background-color 0.2s; backdrop-filter: blur(4px); `; // Add hover effect toggleBtn.addEventListener('mouseenter', () => { toggleBtn.style.opacity = '1'; toggleBtn.style.background = currentTheme === 'light' ? 'rgba(246, 248, 250, 0.8)' : 'rgba(22, 27, 34, 0.8)'; toggleBtn.style.color = currentTheme === 'light' ? '#24292f' : '#e6edf3'; }); toggleBtn.addEventListener('mouseleave', () => { toggleBtn.style.opacity = '0.5'; toggleBtn.style.background = 'rgba(31, 35, 40, 0.6)'; toggleBtn.style.color = '#adbac7'; }); // Toggle labels on click toggleBtn.addEventListener('click', () => { USER_CONFIG.labelsVisible = !USER_CONFIG.labelsVisible; GM_setValue('commitLabelsConfig', USER_CONFIG); // Update button toggleBtn.textContent = USER_CONFIG.labelsVisible ? '🏷️' : '🏷️'; toggleBtn.style.textDecoration = USER_CONFIG.labelsVisible ? 'none' : 'line-through'; toggleBtn.title = USER_CONFIG.labelsVisible ? 'Hide commit labels' : 'Show commit labels'; // Toggle label visibility document.querySelectorAll('.commit-label').forEach(label => { label.style.display = USER_CONFIG.labelsVisible ? 'inline-flex' : 'none'; }); }); document.body.appendChild(toggleBtn); // Set initial state toggleBtn.style.textDecoration = USER_CONFIG.labelsVisible ? 'none' : 'line-through'; } // Create configuration window function createConfigWindow() { // Get current theme colors for the config window const isDark = isDarkTheme(currentTheme); const configStyles = { window: { background: isDark ? '#0d1117' : '#ffffff', border: isDark ? '1px solid #30363d' : '1px solid #d0d7de', color: isDark ? '#c9d1d9' : '#24292f', boxShadow: isDark ? '0 0 10px rgba(0,0,0,0.5)' : '0 0 10px rgba(0,0,0,0.2)' }, button: { primary: { background: '#238636', color: '#ffffff', border: 'none' }, secondary: { background: isDark ? '#21262d' : '#f6f8fa', color: isDark ? '#c9d1d9' : '#24292f', border: isDark ? '1px solid #30363d' : '1px solid #d0d7de' }, danger: { background: isDark ? '#21262d' : '#f6f8fa', color: '#f85149', border: isDark ? '1px solid #30363d' : '1px solid #d0d7de' } }, input: { background: isDark ? '#161b22' : '#f6f8fa', color: isDark ? '#c9d1d9' : '#24292f', border: isDark ? '1px solid #30363d' : '1px solid #d0d7de' }, text: { dim: isDark ? '#8b949e' : '#6e7781', link: isDark ? '#58a6ff' : '#0969da' } }; const configWindow = document.createElement('div'); configWindow.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: ${configStyles.window.background}; border: ${configStyles.window.border}; border-radius: 6px; padding: 20px; z-index: 9999; width: 600px; max-height: 80vh; overflow-y: auto; color: ${configStyles.window.color}; box-shadow: ${configStyles.window.boxShadow}; `; // Header with title and repo link const titleContainer = document.createElement('div'); titleContainer.style.display = 'flex'; titleContainer.style.justifyContent = 'space-between'; titleContainer.style.alignItems = 'center'; titleContainer.style.marginBottom = '20px'; const title = document.createElement('h2'); title.textContent = 'Commit Labels Configuration'; title.style.margin = '0'; // Repository link container with profile pic const repoContainer = document.createElement('div'); repoContainer.style.display = 'flex'; repoContainer.style.alignItems = 'center'; repoContainer.style.gap = '8px'; // Owner profile picture const profilePic = document.createElement('img'); profilePic.src = 'https://raw.githubusercontent.com/nazdridoy/nazhome/main/public/favicons/nazhome.svg'; profilePic.alt = 'Owner'; profilePic.style.cssText = ` width: 36px; height: 36px; border-radius: 50%; background: ${isDark ? '#30363d' : '#eaeef2'}; padding: 3px; vertical-align: middle; `; const repoLink = document.createElement('a'); repoLink.href = 'https://github.com/nazdridoy/github-commit-labels'; repoLink.target = '_blank'; repoLink.textContent = 'GitHub Repository'; repoLink.style.cssText = ` color: ${configStyles.text.link}; text-decoration: none; font-size: 15px; vertical-align: middle; `; repoLink.addEventListener('mouseenter', () => { repoLink.style.textDecoration = 'underline'; }); repoLink.addEventListener('mouseleave', () => { repoLink.style.textDecoration = 'none'; }); repoContainer.appendChild(profilePic); repoContainer.appendChild(repoLink); titleContainer.appendChild(title); titleContainer.appendChild(repoContainer); configWindow.appendChild(titleContainer); // Remove Prefix Option const prefixDiv = document.createElement('div'); prefixDiv.style.marginBottom = '20px'; const prefixCheckbox = document.createElement('input'); prefixCheckbox.type = 'checkbox'; prefixCheckbox.checked = USER_CONFIG.removePrefix; prefixCheckbox.id = 'remove-prefix'; prefixCheckbox.style.marginRight = '5px'; const prefixLabel = document.createElement('label'); prefixLabel.htmlFor = 'remove-prefix'; prefixLabel.textContent = 'Remove commit type prefix from message'; prefixDiv.appendChild(prefixCheckbox); prefixDiv.appendChild(prefixLabel); configWindow.appendChild(prefixDiv); // Add toggle for tooltips with preview const tooltipDiv = document.createElement('div'); tooltipDiv.style.marginBottom = '20px'; const tooltipHeader = document.createElement('div'); tooltipHeader.style.display = 'flex'; tooltipHeader.style.alignItems = 'center'; tooltipHeader.style.marginBottom = '5px'; const tooltipCheckbox = document.createElement('input'); tooltipCheckbox.type = 'checkbox'; tooltipCheckbox.checked = USER_CONFIG.enableTooltips; tooltipCheckbox.id = 'enable-tooltips'; tooltipCheckbox.style.marginRight = '5px'; const tooltipLabel = document.createElement('label'); tooltipLabel.htmlFor = 'enable-tooltips'; tooltipLabel.textContent = 'Enable tooltips with extended descriptions'; tooltipLabel.style.marginRight = '15px'; // Add tooltip preview const previewLabel = document.createElement('span'); previewLabel.textContent = 'Preview: '; previewLabel.style.marginRight = '5px'; const previewExample = document.createElement('span'); previewExample.className = 'tooltip-preview-label'; previewExample.innerHTML = '✨ <span>Feature</span>'; previewExample.dataset.description = 'New user features (not for new files without user features)'; previewExample.style.cssText = ` display: inline-flex; align-items: center; justify-content: center; height: 24px; padding: 0 10px; border-radius: 20px; background: ${isDark ? 'rgba(35, 134, 54, 0.2)' : 'rgba(31, 136, 61, 0.1)'}; color: ${isDark ? '#7ee787' : '#1a7f37'}; cursor: help; `; tooltipHeader.appendChild(tooltipCheckbox); tooltipHeader.appendChild(tooltipLabel); tooltipHeader.appendChild(previewLabel); tooltipHeader.appendChild(previewExample); // Create custom preview tooltip previewExample.addEventListener('mouseenter', (e) => { if (!tooltipCheckbox.checked) return; const tooltipPreview = document.createElement('div'); tooltipPreview.className = 'tooltip-preview'; tooltipPreview.textContent = previewExample.dataset.description; const rect = e.target.getBoundingClientRect(); tooltipPreview.style.cssText = ` position: fixed; top: ${rect.bottom + 5}px; left: ${rect.left}px; max-width: 300px; padding: 8px 12px; color: ${isDark ? '#e6edf3' : '#ffffff'}; text-align: center; background-color: ${isDark ? '#161b22' : '#24292f'}; border-radius: 6px; border: ${isDark ? '1px solid #30363d' : '1px solid #d0d7de'}; box-shadow: 0 3px 12px rgba(0,0,0,0.4); font-size: 12px; z-index: 10000; pointer-events: none; `; document.body.appendChild(tooltipPreview); }); previewExample.addEventListener('mouseleave', () => { const tooltipPreview = document.querySelector('.tooltip-preview'); if (tooltipPreview) { document.body.removeChild(tooltipPreview); } }); tooltipDiv.appendChild(tooltipHeader); // Add explanation text const tooltipExplanation = document.createElement('div'); tooltipExplanation.textContent = 'Tooltips show detailed descriptions when hovering over commit labels.'; tooltipExplanation.style.color = configStyles.text.dim; tooltipExplanation.style.fontSize = '12px'; tooltipExplanation.style.marginTop = '5px'; tooltipDiv.appendChild(tooltipExplanation); configWindow.insertBefore(tooltipDiv, prefixDiv.nextSibling); // After prefixDiv and tooltipDiv, add a toggle for showing the floating button const floatingBtnDiv = document.createElement('div'); floatingBtnDiv.style.marginBottom = '20px'; // Add showFloatingButton to USER_CONFIG if it doesn't exist if (USER_CONFIG.showFloatingButton === undefined) { USER_CONFIG.showFloatingButton = true; GM_setValue('commitLabelsConfig', USER_CONFIG); } const floatingBtnCheckbox = document.createElement('input'); floatingBtnCheckbox.type = 'checkbox'; floatingBtnCheckbox.checked = USER_CONFIG.showFloatingButton; floatingBtnCheckbox.id = 'show-floating-btn'; floatingBtnCheckbox.style.marginRight = '5px'; const floatingBtnLabel = document.createElement('label'); floatingBtnLabel.htmlFor = 'show-floating-btn'; floatingBtnLabel.textContent = 'Show floating toggle button'; floatingBtnDiv.appendChild(floatingBtnCheckbox); floatingBtnDiv.appendChild(floatingBtnLabel); configWindow.insertBefore(floatingBtnDiv, tooltipDiv.nextSibling); // Commit Types Configuration const typesContainer = document.createElement('div'); typesContainer.style.marginBottom = '20px'; // Group commit types by their label const groupedTypes = {}; Object.entries(USER_CONFIG.commitTypes).forEach(([type, config]) => { const key = config.label; if (!groupedTypes[key]) { groupedTypes[key] = { types: [], config: config }; } groupedTypes[key].types.push(type); }); // Create rows for grouped types Object.entries(groupedTypes).forEach(([label, { types, config }]) => { const typeDiv = document.createElement('div'); typeDiv.style.marginBottom = '10px'; typeDiv.style.display = 'flex'; typeDiv.style.alignItems = 'center'; typeDiv.style.gap = '10px'; // Type names (with aliases) and edit button container const typeContainer = document.createElement('div'); typeContainer.style.display = 'flex'; typeContainer.style.width = '150px'; typeContainer.style.alignItems = 'center'; typeContainer.style.gap = '4px'; const typeSpan = document.createElement('span'); typeSpan.style.color = configStyles.text.dim; typeSpan.style.flex = '1'; typeSpan.textContent = types.join(', ') + ':'; const editAliasButton = document.createElement('button'); editAliasButton.textContent = '✏️'; editAliasButton.title = 'Edit Aliases'; editAliasButton.style.cssText = ` padding: 2px 4px; background: ${configStyles.button.secondary.background}; color: ${isDark ? '#58a6ff' : '#0969da'}; border: ${configStyles.button.secondary.border}; border-radius: 4px; cursor: pointer; font-size: 10px; `; editAliasButton.onclick = () => { const currentAliases = types.join(', '); const newAliases = prompt('Edit aliases (separate with commas):', currentAliases); if (newAliases && newAliases.trim()) { const newTypes = newAliases.split(',').map(t => t.trim().toLowerCase()).filter(t => t); // Check if any new aliases conflict with other types const conflictingType = newTypes.find(type => USER_CONFIG.commitTypes[type] && !types.includes(type) ); if (conflictingType) { alert(`The alias "${conflictingType}" already exists in another group!`); return; } // Remove old types types.forEach(type => delete USER_CONFIG.commitTypes[type]); // Add new types with same config newTypes.forEach(type => { USER_CONFIG.commitTypes[type] = { ...config }; }); // Update the display typeSpan.textContent = newTypes.join(', ') + ':'; // Update dataset for inputs const inputs = typeDiv.querySelectorAll('input, select'); inputs.forEach(input => { input.dataset.types = newTypes.join(','); }); } }; typeContainer.appendChild(typeSpan); typeContainer.appendChild(editAliasButton); typeDiv.appendChild(typeContainer); // Emoji input const emojiInput = document.createElement('input'); emojiInput.type = 'text'; emojiInput.value = config.emoji; emojiInput.style.width = '40px'; emojiInput.style.background = configStyles.input.background; emojiInput.style.color = configStyles.input.color; emojiInput.style.border = configStyles.input.border; emojiInput.style.borderRadius = '4px'; emojiInput.style.padding = '4px'; emojiInput.dataset.types = types.join(','); emojiInput.dataset.field = 'emoji'; typeDiv.appendChild(emojiInput); // Label input const labelInput = document.createElement('input'); labelInput.type = 'text'; labelInput.value = config.label; labelInput.style.width = '120px'; labelInput.style.background = configStyles.input.background; labelInput.style.color = configStyles.input.color; labelInput.style.border = configStyles.input.border; labelInput.style.borderRadius = '4px'; labelInput.style.padding = '4px'; labelInput.dataset.types = types.join(','); labelInput.dataset.field = 'label'; typeDiv.appendChild(labelInput); // Color select const colorSelect = document.createElement('select'); Object.keys(COLORS).forEach(color => { const option = document.createElement('option'); option.value = color; option.textContent = color; if (config.color === color) option.selected = true; colorSelect.appendChild(option); }); colorSelect.style.background = configStyles.input.background; colorSelect.style.color = configStyles.input.color; colorSelect.style.border = configStyles.input.border; colorSelect.style.borderRadius = '4px'; colorSelect.style.padding = '4px'; colorSelect.dataset.types = types.join(','); colorSelect.dataset.field = 'color'; typeDiv.appendChild(colorSelect); // Delete button const deleteButton = document.createElement('button'); deleteButton.textContent = '🗑️'; deleteButton.style.cssText = ` padding: 2px 8px; background: ${configStyles.button.danger.background}; color: ${configStyles.button.danger.color}; border: ${configStyles.button.danger.border}; border-radius: 4px; cursor: pointer; `; deleteButton.onclick = () => { if (confirm(`Delete commit types "${types.join(', ')}"?`)) { typeDiv.remove(); types.forEach(type => delete USER_CONFIG.commitTypes[type]); } }; typeDiv.appendChild(deleteButton); typesContainer.appendChild(typeDiv); }); // Add "Add New Type" button const addNewButton = document.createElement('button'); addNewButton.textContent = '+ Add New Type'; addNewButton.style.cssText = ` margin-bottom: 15px; padding: 5px 16px; background: ${configStyles.button.primary.background}; color: ${configStyles.button.primary.color}; border: ${configStyles.button.primary.border}; border-radius: 6px; cursor: pointer; `; addNewButton.onclick = () => { const typeInput = prompt('Enter the commit type and aliases (separated by commas, e.g., "added, add"):', ''); if (typeInput && typeInput.trim()) { const types = typeInput.split(',').map(t => t.trim().toLowerCase()).filter(t => t); // Check if any of the types already exist const existingType = types.find(type => USER_CONFIG.commitTypes[type]); if (existingType) { alert(`The commit type "${existingType}" already exists!`); return; } // Create base config for all aliases const baseConfig = { emoji: '🔄', label: types[0].charAt(0).toUpperCase() + types[0].slice(1), color: 'blue', description: 'Custom commit type' }; // Add all types to config with the same settings types.forEach(type => { USER_CONFIG.commitTypes[type] = { ...baseConfig }; }); // Create and add new type row const typeDiv = document.createElement('div'); typeDiv.style.marginBottom = '10px'; typeDiv.style.display = 'flex'; typeDiv.style.alignItems = 'center'; typeDiv.style.gap = '10px'; // Type names (with aliases) const typeSpan = document.createElement('span'); typeSpan.style.width = '150px'; typeSpan.style.color = configStyles.text.dim; typeSpan.textContent = types.join(', ') + ':'; typeDiv.appendChild(typeSpan); // Emoji input const emojiInput = document.createElement('input'); emojiInput.type = 'text'; emojiInput.value = baseConfig.emoji; emojiInput.style.width = '40px'; emojiInput.style.background = configStyles.input.background; emojiInput.style.color = configStyles.input.color; emojiInput.style.border = configStyles.input.border; emojiInput.style.borderRadius = '4px'; emojiInput.style.padding = '4px'; emojiInput.dataset.types = types.join(','); emojiInput.dataset.field = 'emoji'; typeDiv.appendChild(emojiInput); // Label input const labelInput = document.createElement('input'); labelInput.type = 'text'; labelInput.value = baseConfig.label; labelInput.style.width = '120px'; labelInput.style.background = configStyles.input.background; labelInput.style.color = configStyles.input.color; labelInput.style.border = configStyles.input.border; labelInput.style.borderRadius = '4px'; labelInput.style.padding = '4px'; labelInput.dataset.types = types.join(','); labelInput.dataset.field = 'label'; typeDiv.appendChild(labelInput); // Color select const colorSelect = document.createElement('select'); Object.keys(COLORS).forEach(color => { const option = document.createElement('option'); option.value = color; option.textContent = color; if (color === 'blue') option.selected = true; colorSelect.appendChild(option); }); colorSelect.style.background = configStyles.input.background; colorSelect.style.color = configStyles.input.color; colorSelect.style.border = configStyles.input.border; colorSelect.style.borderRadius = '4px'; colorSelect.style.padding = '4px'; colorSelect.dataset.types = types.join(','); colorSelect.dataset.field = 'color'; typeDiv.appendChild(colorSelect); // Delete button const deleteButton = document.createElement('button'); deleteButton.textContent = '🗑️'; deleteButton.style.cssText = ` padding: 2px 8px; background: ${configStyles.button.danger.background}; color: ${configStyles.button.danger.color}; border: ${configStyles.button.danger.border}; border-radius: 4px; cursor: pointer; `; deleteButton.onclick = () => { if (confirm(`Delete commit types "${types.join(', ')}"?`)) { typeDiv.remove(); types.forEach(type => delete USER_CONFIG.commitTypes[type]); } }; typeDiv.appendChild(deleteButton); typesContainer.appendChild(typeDiv); } }; configWindow.appendChild(addNewButton); configWindow.appendChild(typesContainer); // Save and Close buttons const buttonContainer = document.createElement('div'); buttonContainer.style.display = 'flex'; buttonContainer.style.gap = '10px'; buttonContainer.style.justifyContent = 'flex-end'; const saveButton = document.createElement('button'); saveButton.textContent = 'Save'; saveButton.style.cssText = ` padding: 5px 16px; background: ${configStyles.button.primary.background}; color: ${configStyles.button.primary.color}; border: ${configStyles.button.primary.border}; border-radius: 6px; cursor: pointer; `; const closeButton = document.createElement('button'); closeButton.textContent = 'Close'; closeButton.style.cssText = ` padding: 5px 16px; background: ${configStyles.button.secondary.background}; color: ${configStyles.button.secondary.color}; border: ${configStyles.button.secondary.border}; border-radius: 6px; cursor: pointer; `; // Add Reset button next to Save and Close const resetButton = document.createElement('button'); resetButton.textContent = 'Reset to Default'; resetButton.style.cssText = ` padding: 5px 16px; background: ${configStyles.button.danger.background}; color: ${configStyles.button.danger.color}; border: ${configStyles.button.danger.border}; border-radius: 6px; cursor: pointer; margin-right: auto; // This pushes Save/Close to the right `; resetButton.onclick = () => { if (confirm('Are you sure you want to reset all settings to default? This will remove all custom types and settings.')) { GM_setValue('commitLabelsConfig', DEFAULT_CONFIG); location.reload(); } }; saveButton.onclick = () => { const newConfig = { ...USER_CONFIG }; newConfig.removePrefix = prefixCheckbox.checked; newConfig.enableTooltips = tooltipCheckbox.checked; newConfig.showFloatingButton = floatingBtnCheckbox.checked; newConfig.commitTypes = {}; typesContainer.querySelectorAll('input, select').forEach(input => { const types = input.dataset.types.split(','); const field = input.dataset.field; types.forEach(type => { if (!newConfig.commitTypes[type]) { newConfig.commitTypes[type] = {}; } newConfig.commitTypes[type][field] = input.value; }); }); GM_setValue('commitLabelsConfig', newConfig); location.reload(); }; closeButton.onclick = () => { document.body.removeChild(configWindow); }; buttonContainer.appendChild(resetButton); buttonContainer.appendChild(closeButton); buttonContainer.appendChild(saveButton); configWindow.appendChild(buttonContainer); document.body.appendChild(configWindow); } // Create export/import dialog function createExportImportDialog() { // Check if dialog already exists if (document.getElementById('config-export-import')) { document.getElementById('config-export-import').remove(); } const dialog = document.createElement('div'); dialog.id = 'config-export-import'; dialog.style.cssText = ` position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #0d1117; border: 1px solid #30363d; border-radius: 6px; padding: 20px; z-index: 9999; width: 500px; max-height: 80vh; overflow-y: auto; color: #c9d1d9; box-shadow: 0 0 20px rgba(0,0,0,0.7); `; const title = document.createElement('h2'); title.textContent = 'Export/Import Configuration'; title.style.marginBottom = '15px'; const exportSection = document.createElement('div'); exportSection.style.marginBottom = '20px'; const exportTitle = document.createElement('h3'); exportTitle.textContent = 'Export Configuration'; exportTitle.style.marginBottom = '10px'; const configOutput = document.createElement('textarea'); configOutput.readOnly = true; configOutput.value = JSON.stringify(USER_CONFIG, null, 2); configOutput.style.cssText = ` width: 100%; height: 150px; background: #161b22; color: #c9d1d9; border: 1px solid #30363d; border-radius: 6px; padding: 10px; font-family: monospace; resize: vertical; margin-bottom: 10px; `; const copyButton = document.createElement('button'); copyButton.textContent = 'Copy to Clipboard'; copyButton.style.cssText = ` padding: 6px 16px; background: #238636; color: #fff; border: none; border-radius: 6px; cursor: pointer; margin-right: 10px; `; copyButton.onclick = () => { configOutput.select(); document.execCommand('copy'); copyButton.textContent = 'Copied!'; setTimeout(() => { copyButton.textContent = 'Copy to Clipboard'; }, 2000); }; exportSection.appendChild(exportTitle); exportSection.appendChild(configOutput); exportSection.appendChild(copyButton); const importSection = document.createElement('div'); importSection.style.marginBottom = '20px'; const importTitle = document.createElement('h3'); importTitle.textContent = 'Import Configuration'; importTitle.style.marginBottom = '10px'; const configInput = document.createElement('textarea'); configInput.placeholder = 'Paste configuration JSON here...'; configInput.style.cssText = ` width: 100%; height: 150px; background: #161b22; color: #c9d1d9; border: 1px solid #30363d; border-radius: 6px; padding: 10px; font-family: monospace; resize: vertical; margin-bottom: 10px; `; const importButton = document.createElement('button'); importButton.textContent = 'Import'; importButton.style.cssText = ` padding: 6px 16px; background: #238636; color: #fff; border: none; border-radius: 6px; cursor: pointer; margin-right: 10px; `; importButton.onclick = () => { try { const newConfig = JSON.parse(configInput.value); // Validate basic structure if (!newConfig.commitTypes) { throw new Error('Invalid configuration: missing commitTypes object'); } if (confirm('Are you sure you want to import this configuration? This will overwrite your current settings.')) { GM_setValue('commitLabelsConfig', newConfig); alert('Configuration imported successfully! Page will reload to apply changes.'); location.reload(); } } catch (error) { alert('Error importing configuration: ' + error.message); } }; const closeButton = document.createElement('button'); closeButton.textContent = 'Close'; closeButton.style.cssText = ` padding: 6px 16px; background: #21262d; color: #c9d1d9; border: 1px solid #30363d; border-radius: 6px; cursor: pointer; `; closeButton.onclick = () => { document.body.removeChild(dialog); }; importSection.appendChild(importTitle); importSection.appendChild(configInput); importSection.appendChild(importButton); dialog.appendChild(title); dialog.appendChild(exportSection); dialog.appendChild(importSection); dialog.appendChild(closeButton); document.body.appendChild(dialog); } // Register configuration menu command GM_registerMenuCommand('Configure Commit Labels', createConfigWindow); GM_registerMenuCommand('Toggle Labels', () => { USER_CONFIG.labelsVisible = !USER_CONFIG.labelsVisible; GM_setValue('commitLabelsConfig', USER_CONFIG); // Toggle label visibility document.querySelectorAll('.commit-label').forEach(label => { label.style.display = USER_CONFIG.labelsVisible ? 'inline-flex' : 'none'; }); // Update toggle button if it exists const toggleBtn = document.getElementById('commit-labels-toggle'); if (toggleBtn) { toggleBtn.textContent = USER_CONFIG.labelsVisible ? '🏷️' : '🏷️'; toggleBtn.style.textDecoration = USER_CONFIG.labelsVisible ? 'none' : 'line-through'; toggleBtn.title = USER_CONFIG.labelsVisible ? 'Hide commit labels' : 'Show commit labels'; } }); GM_registerMenuCommand('Export/Import Config', createExportImportDialog); // Check if we're on a commit page function isCommitPage() { return window.location.pathname.includes('/commits') || window.location.pathname.includes('/commit/'); } // Update colors when theme changes function updateThemeColors() { const newTheme = detectTheme(); if (newTheme !== currentTheme) { currentTheme = newTheme; // Map theme variants to our base themes for colors let baseTheme = newTheme; if (newTheme.startsWith('light_')) { baseTheme = 'light'; } else if (newTheme.startsWith('dark_') && newTheme !== 'dark_dimmed') { baseTheme = 'dark'; } COLORS = THEME_COLORS[baseTheme] || THEME_COLORS.light; // Update existing labels document.querySelectorAll('.commit-label').forEach(label => { const type = label.dataset.commitType; if (type && USER_CONFIG.commitTypes[type]) { const color = COLORS[USER_CONFIG.commitTypes[type].color]; if (color) { label.style.backgroundColor = color.bg; label.style.color = color.text; } } }); } } function addCommitLabels() { // Only proceed if we're on a commit page if (!isCommitPage()) return; // Update theme colors updateThemeColors(); // Create toggle button if it doesn't exist and is enabled if (USER_CONFIG.showFloatingButton !== false) { createLabelToggle(); } // Update selector to match GitHub's current DOM structure const commitMessages = document.querySelectorAll('.markdown-title a[data-pjax="true"]'); // Debounce and batch process for performance improvement let processedCount = 0; const batchSize = 20; const commitMessagesArray = Array.from(commitMessages); const processCommitBatch = (startIndex) => { const endIndex = Math.min(startIndex + batchSize, commitMessagesArray.length); for (let i = startIndex; i < endIndex; i++) { const message = commitMessagesArray[i]; const text = message.textContent.trim(); const match = text.match(/^(\w+)(?:\([\w-]+\))?:\s*(.*)/); if (match) { const type = match[1].toLowerCase(); const restOfMessage = match[2]; if (USER_CONFIG.commitTypes[type]) { // Only add label if it hasn't been added yet if (!message.parentElement.querySelector('.commit-label')) { const label = document.createElement('span'); label.className = 'commit-label'; label.dataset.commitType = type; const color = COLORS[USER_CONFIG.commitTypes[type].color]; // Apply styles const styles = { ...USER_CONFIG.labelStyle, backgroundColor: color.bg, color: color.text, display: USER_CONFIG.labelsVisible ? 'inline-flex' : 'none' }; label.style.cssText = Object.entries(styles) .map(([key, value]) => `${key.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${value}`) .join(';'); // Enhanced tooltip if (USER_CONFIG.enableTooltips && USER_CONFIG.commitTypes[type].description) { // Store description in data attribute instead of title to avoid double tooltips label.dataset.description = USER_CONFIG.commitTypes[type].description; label.setAttribute('aria-label', USER_CONFIG.commitTypes[type].description); // Add tooltip indicator label.style.cursor = 'help'; // For better accessibility label.setAttribute('role', 'tooltip'); // Create a custom tooltip implementation if needed label.addEventListener('mouseenter', (e) => { // Check if we already have a custom tooltip showing if (document.querySelector('.commit-label-tooltip')) { return; } label.style.transform = 'translateY(-1px)'; label.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)'; // Force show tooltip by creating a custom one if (label.dataset.description) { const tooltip = document.createElement('div'); tooltip.className = 'commit-label-tooltip'; tooltip.textContent = label.dataset.description; // Calculate position relative to viewport const rect = e.target.getBoundingClientRect(); const top = rect.bottom + 5; const left = rect.left; tooltip.style.cssText = ` position: fixed; top: ${top}px; left: ${left}px; max-width: 300px; padding: 8px 12px; color: #e6edf3; text-align: center; background-color: #161b22; border-radius: 6px; border: 1px solid #30363d; box-shadow: 0 3px 12px rgba(0,0,0,0.4); font-size: 12px; z-index: 1000; pointer-events: none; `; document.body.appendChild(tooltip); // Adjust position if tooltip goes off-screen const tooltipRect = tooltip.getBoundingClientRect(); if (tooltipRect.right > window.innerWidth) { tooltip.style.left = `${window.innerWidth - tooltipRect.width - 10}px`; } } }); label.addEventListener('mouseleave', () => { label.style.transform = 'translateY(0)'; label.style.boxShadow = styles.boxShadow; // Remove custom tooltip if it exists const tooltip = document.querySelector('.commit-label-tooltip'); if (tooltip) { document.body.removeChild(tooltip); } }); } else { // Normal hover effect if tooltips are disabled if (USER_CONFIG.commitTypes[type].description) { label.title = USER_CONFIG.commitTypes[type].description; } label.addEventListener('mouseenter', () => { label.style.transform = 'translateY(-1px)'; label.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)'; }); label.addEventListener('mouseleave', () => { label.style.transform = 'translateY(0)'; label.style.boxShadow = styles.boxShadow; }); } const emoji = document.createElement('span'); emoji.style.marginRight = '4px'; emoji.style.fontSize = '14px'; emoji.style.lineHeight = '1'; emoji.textContent = USER_CONFIG.commitTypes[type].emoji; const labelText = document.createElement('span'); labelText.textContent = USER_CONFIG.commitTypes[type].label; label.appendChild(emoji); label.appendChild(labelText); message.parentElement.insertBefore(label, message); // Update the commit message text to remove the type prefix if enabled if (USER_CONFIG.removePrefix) { message.textContent = restOfMessage; } } } } } // Process next batch if needed processedCount += (endIndex - startIndex); if (processedCount < commitMessagesArray.length) { setTimeout(() => processCommitBatch(endIndex), 0); } }; // Start processing first batch if (commitMessagesArray.length > 0) { processCommitBatch(0); } } // Only set up observers if we're on a commit page function initialize() { // Initial run addCommitLabels(); // Watch for DOM changes const observer = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.addedNodes.length) { addCommitLabels(); } } }); // Start observing the document with the configured parameters observer.observe(document.body, { childList: true, subtree: true }); // Watch for theme changes const themeObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.attributeName === 'data-color-mode' || mutation.attributeName === 'data-dark-theme' || mutation.attributeName === 'data-light-theme') { updateThemeColors(); } } }); // Start observing the html element for theme changes themeObserver.observe(document.documentElement, { attributes: true }); } // Initialize on page load initialize(); // Handle GitHub's client-side navigation const navigationObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === 'childList') { // Check if we're on a commit page after navigation if (isCommitPage()) { // Small delay to ensure GitHub has finished rendering setTimeout(addCommitLabels, 100); } } } }); // Observe changes to the main content area navigationObserver.observe(document.body, { childList: true, subtree: true }); // Listen for popstate events (browser back/forward navigation) window.addEventListener('popstate', () => { if (isCommitPage()) { setTimeout(addCommitLabels, 100); } }); // Listen for GitHub's custom navigation event document.addEventListener('turbo:render', () => { if (isCommitPage()) { setTimeout(addCommitLabels, 100); } }); })();