GitHub Commit Labels

Enhances GitHub commits with beautiful labels for conventional commit types (feat, fix, docs, etc.)

  1. // ==UserScript==
  2. // @name GitHub Commit Labels
  3. // @namespace https://github.com/nazdridoy
  4. // @version 1.6.0
  5. // @description Enhances GitHub commits with beautiful labels for conventional commit types (feat, fix, docs, etc.)
  6. // @author nazdridoy
  7. // @license MIT
  8. // @match https://github.com/*
  9. // @icon https://github.githubassets.com/favicons/favicon.svg
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_registerMenuCommand
  13. // @run-at document-end
  14. // @homepageURL https://github.com/nazdridoy/github-commit-labels
  15. // @supportURL https://github.com/nazdridoy/github-commit-labels/issues
  16. // ==/UserScript==
  17.  
  18. /*
  19. MIT License
  20.  
  21. Copyright (c) 2025 nazDridoy
  22.  
  23. Permission is hereby granted, free of charge, to any person obtaining a copy
  24. of this software and associated documentation files (the "Software"), to deal
  25. in the Software without restriction, including without limitation the rights
  26. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  27. copies of the Software, and to permit persons to whom the Software is
  28. furnished to do so, subject to the following conditions:
  29.  
  30. The above copyright notice and this permission notice shall be included in all
  31. copies or substantial portions of the Software.
  32.  
  33. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  34. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  35. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  36. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  37. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  38. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  39. SOFTWARE.
  40. */
  41.  
  42. (function() {
  43. 'use strict';
  44.  
  45. // Detect GitHub theme (dark, light, or dark dimmed)
  46. function detectTheme() {
  47. const html = document.documentElement;
  48. const colorMode = html.getAttribute('data-color-mode');
  49. // Handle sync with system (auto) setting
  50. if (colorMode === 'auto') {
  51. // Get the system preference
  52. const darkThemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
  53. const isDarkMode = darkThemeMedia.matches;
  54. if (isDarkMode) {
  55. // System is in dark mode, but we need to check what's set for "Night theme"
  56. const darkThemeSetting = html.getAttribute('data-dark-theme');
  57. // If a light theme variant is set for "Night theme"
  58. if (darkThemeSetting && darkThemeSetting.startsWith('light')) {
  59. return darkThemeSetting; // Return the specific light theme variant
  60. }
  61. // Otherwise return the dark theme variant
  62. return darkThemeSetting === 'dark_dimmed' ? 'dark_dimmed' : 'dark';
  63. } else {
  64. // System is in light mode, check what's set for "Day theme"
  65. const lightThemeSetting = html.getAttribute('data-light-theme');
  66. // If a dark theme variant is set for "Day theme"
  67. if (lightThemeSetting && lightThemeSetting.startsWith('dark')) {
  68. return lightThemeSetting; // Return the specific dark theme variant
  69. }
  70. return 'light'; // Default to light theme
  71. }
  72. }
  73. // Direct theme setting (not auto)
  74. if (colorMode === 'dark') {
  75. const darkTheme = html.getAttribute('data-dark-theme');
  76. return darkTheme === 'dark_dimmed' ? 'dark_dimmed' : 'dark';
  77. } else {
  78. const lightTheme = html.getAttribute('data-light-theme');
  79. // If a specific light theme variant is set
  80. if (lightTheme && lightTheme !== 'light') {
  81. return lightTheme;
  82. }
  83. return 'light';
  84. }
  85. }
  86.  
  87. // Helper function to determine if a theme is a dark variant
  88. function isDarkTheme(theme) {
  89. return theme && (theme === 'dark' || theme === 'dark_dimmed' ||
  90. theme === 'dark_high_contrast' || theme === 'dark_colorblind' ||
  91. theme === 'dark_tritanopia');
  92. }
  93.  
  94. // Get current theme
  95. let currentTheme = detectTheme();
  96. // Watch for system color scheme changes
  97. const darkThemeMedia = window.matchMedia('(prefers-color-scheme: dark)');
  98. darkThemeMedia.addEventListener('change', () => {
  99. if (document.documentElement.getAttribute('data-color-mode') === 'auto') {
  100. updateThemeColors();
  101. }
  102. });
  103. // Color definitions based on theme
  104. const THEME_COLORS = {
  105. light: {
  106. 'green': { bg: 'rgba(35, 134, 54, 0.1)', text: '#1a7f37' },
  107. 'purple': { bg: 'rgba(163, 113, 247, 0.1)', text: '#8250df' },
  108. 'blue': { bg: 'rgba(47, 129, 247, 0.1)', text: '#0969da' },
  109. 'light-blue': { bg: 'rgba(31, 111, 235, 0.1)', text: '#0550ae' },
  110. 'yellow': { bg: 'rgba(210, 153, 34, 0.1)', text: '#9e6a03' },
  111. 'orange': { bg: 'rgba(219, 109, 40, 0.1)', text: '#bc4c00' },
  112. 'gray': { bg: 'rgba(139, 148, 158, 0.1)', text: '#57606a' },
  113. 'light-green': { bg: 'rgba(57, 211, 83, 0.1)', text: '#1a7f37' },
  114. 'red': { bg: 'rgba(248, 81, 73, 0.1)', text: '#cf222e' },
  115. 'dark-yellow': { bg: 'rgba(187, 128, 9, 0.1)', text: '#9e6a03' }
  116. },
  117. dark: {
  118. 'green': { bg: 'rgba(35, 134, 54, 0.2)', text: '#7ee787' },
  119. 'purple': { bg: 'rgba(163, 113, 247, 0.2)', text: '#d2a8ff' },
  120. 'blue': { bg: 'rgba(47, 129, 247, 0.2)', text: '#79c0ff' },
  121. 'light-blue': { bg: 'rgba(31, 111, 235, 0.2)', text: '#58a6ff' },
  122. 'yellow': { bg: 'rgba(210, 153, 34, 0.2)', text: '#e3b341' },
  123. 'orange': { bg: 'rgba(219, 109, 40, 0.2)', text: '#ffa657' },
  124. 'gray': { bg: 'rgba(139, 148, 158, 0.2)', text: '#8b949e' },
  125. 'light-green': { bg: 'rgba(57, 211, 83, 0.2)', text: '#56d364' },
  126. 'red': { bg: 'rgba(248, 81, 73, 0.2)', text: '#ff7b72' },
  127. 'dark-yellow': { bg: 'rgba(187, 128, 9, 0.2)', text: '#bb8009' }
  128. },
  129. dark_dimmed: {
  130. 'green': { bg: 'rgba(35, 134, 54, 0.15)', text: '#6bc46d' },
  131. 'purple': { bg: 'rgba(163, 113, 247, 0.15)', text: '#c297ff' },
  132. 'blue': { bg: 'rgba(47, 129, 247, 0.15)', text: '#6cb6ff' },
  133. 'light-blue': { bg: 'rgba(31, 111, 235, 0.15)', text: '#539bf5' },
  134. 'yellow': { bg: 'rgba(210, 153, 34, 0.15)', text: '#daaa3f' },
  135. 'orange': { bg: 'rgba(219, 109, 40, 0.15)', text: '#f0883e' },
  136. 'gray': { bg: 'rgba(139, 148, 158, 0.15)', text: '#909dab' },
  137. 'light-green': { bg: 'rgba(57, 211, 83, 0.15)', text: '#6bc46d' },
  138. 'red': { bg: 'rgba(248, 81, 73, 0.15)', text: '#e5534b' },
  139. 'dark-yellow': { bg: 'rgba(187, 128, 9, 0.15)', text: '#daaa3f' }
  140. }
  141. };
  142.  
  143. // Get colors for current theme
  144. let COLORS = THEME_COLORS[currentTheme];
  145.  
  146. // Define default configuration
  147. const DEFAULT_CONFIG = {
  148. removePrefix: true,
  149. enableTooltips: true,
  150. labelsVisible: true,
  151. showScope: false,
  152. debugMode: false, // Add debug mode setting
  153. labelStyle: {
  154. fontSize: '14px',
  155. fontWeight: '500',
  156. height: '24px',
  157. padding: '0 10px',
  158. marginRight: '8px',
  159. borderRadius: '20px',
  160. minWidth: 'auto',
  161. textAlign: 'center',
  162. display: 'inline-flex',
  163. alignItems: 'center',
  164. justifyContent: 'center',
  165. whiteSpace: 'nowrap',
  166. background: 'rgba(0, 0, 0, 0.2)',
  167. backdropFilter: 'blur(4px)',
  168. border: '1px solid rgba(240, 246, 252, 0.1)', // Subtle border
  169. color: '#ffffff'
  170. },
  171. commitTypes: {
  172. // Features
  173. feat: { emoji: '✨', label: 'Feature', color: 'green', description: 'New user features (not for new files without user features)' },
  174. feature: { emoji: '✨', label: 'Feature', color: 'green', description: 'New user features (not for new files without user features)' },
  175.  
  176. // Added
  177. added: { emoji: '📝', label: 'Added', color: 'green', description: 'New files/resources with no user-facing features' },
  178. add: { emoji: '📝', label: 'Added', color: 'green', description: 'New files/resources with no user-facing features' },
  179.  
  180. // Updated
  181. update: { emoji: '♻️', label: 'Updated', color: 'blue', description: 'Changes to existing functionality' },
  182. updated: { emoji: '♻️', label: 'Updated', color: 'blue', description: 'Changes to existing functionality' },
  183.  
  184. // Removed
  185. removed: { emoji: '🗑️', label: 'Removed', color: 'red', description: 'Removing files/code' },
  186. remove: { emoji: '🗑️', label: 'Removed', color: 'red', description: 'Removing files/code' },
  187. delete: { emoji: '🗑️', label: 'Removed', color: 'red', description: 'Removing files/code' },
  188. del: { emoji: '🗑️', label: 'Removed', color: 'red', description: 'Removing files/code' },
  189.  
  190. // Fixes
  191. fix: { emoji: '🐛', label: 'Fix', color: 'purple', description: 'Bug fixes/corrections to errors' },
  192. bugfix: { emoji: '🐛', label: 'Fix', color: 'purple', description: 'Bug fixes/corrections to errors' },
  193. fixed: { emoji: '🐛', label: 'Fix', color: 'purple', description: 'Bug fixes/corrections to errors' },
  194. hotfix: { emoji: '🚨', label: 'Hot Fix', color: 'red', description: 'Critical bug fixes requiring immediate attention' },
  195.  
  196. // Documentation
  197. docs: { emoji: '📚', label: 'Docs', color: 'blue', description: 'Documentation only changes' },
  198. doc: { emoji: '📚', label: 'Docs', color: 'blue', description: 'Documentation only changes' },
  199. documentation: { emoji: '📚', label: 'Docs', color: 'blue', description: 'Documentation only changes' },
  200.  
  201. // Styling
  202. style: { emoji: '💎', label: 'Style', color: 'light-green', description: 'Formatting/whitespace changes (no code change)' },
  203. ui: { emoji: '🎨', label: 'UI', color: 'light-green', description: 'User interface changes' },
  204. css: { emoji: '💎', label: 'Style', color: 'light-green', description: 'CSS/styling changes' },
  205.  
  206. // Code Changes
  207. refactor: { emoji: '📦', label: 'Refactor', color: 'light-blue', description: 'Restructured code (no behavior change)' },
  208. perf: { emoji: '🚀', label: 'Performance', color: 'purple', description: 'Performance improvements' },
  209. performance: { emoji: '🚀', label: 'Performance', color: 'purple', description: 'Performance improvements' },
  210. optimize: { emoji: '⚡', label: 'Optimize', color: 'purple', description: 'Code optimization without functional changes' },
  211.  
  212. // Testing
  213. test: { emoji: '🧪', label: 'Test', color: 'yellow', description: 'Test-related changes' },
  214. tests: { emoji: '🧪', label: 'Test', color: 'yellow', description: 'Test-related changes' },
  215. testing: { emoji: '🧪', label: 'Test', color: 'yellow', description: 'Test-related changes' },
  216.  
  217. // Build & Deploy
  218. build: { emoji: '🛠', label: 'Build', color: 'orange', description: 'Build system changes' },
  219. ci: { emoji: '⚙️', label: 'CI', color: 'gray', description: 'CI pipeline changes' },
  220. cd: { emoji: '🚀', label: 'CD', color: 'gray', description: 'Continuous deployment changes' },
  221. deploy: { emoji: '📦', label: 'Deploy', color: 'orange', description: 'Deployment related changes' },
  222. release: { emoji: '🚀', label: 'Deploy', color: 'orange', description: 'Production releases' },
  223.  
  224. // Maintenance
  225. chore: { emoji: '♻️', label: 'Chore', color: 'light-green', description: 'Routine maintenance tasks' },
  226. deps: { emoji: '📦', label: 'Dependencies', color: 'light-green', description: 'Dependency updates or changes' },
  227. dep: { emoji: '📦', label: 'Dependencies', color: 'light-green', description: 'Dependency updates or changes' },
  228. dependencies: { emoji: '📦', label: 'Dependencies', color: 'light-green', description: 'Dependency updates or changes' },
  229. revert: { emoji: '🗑', label: 'Revert', color: 'red', description: 'Reverting previous changes' },
  230. wip: { emoji: '🚧', label: 'WIP', color: 'dark-yellow', description: 'Work in progress' },
  231.  
  232. // Security
  233. security: { emoji: '🔒', label: 'Security', color: 'red', description: 'Security-related changes' },
  234. // Internationalization
  235. i18n: { emoji: '🌐', label: 'i18n', color: 'blue', description: 'Internationalization and localization' },
  236. // Accessibility
  237. a11y: { emoji: '♿', label: 'Accessibility', color: 'purple', description: 'Accessibility improvements' },
  238. // API changes
  239. api: { emoji: '🔌', label: 'API', color: 'light-blue', description: 'API-related changes' },
  240. // Database changes
  241. data: { emoji: '🗃️', label: 'Database', color: 'orange', description: 'Database schema or data changes' },
  242. // Configuration changes
  243. config: { emoji: '⚙️', label: 'Config', color: 'gray', description: 'Configuration changes' },
  244. // Initial setup
  245. init: { emoji: '🎬', label: 'Init', color: 'green', description: 'Initial commit/project setup' }
  246. }
  247. };
  248.  
  249. // Get saved configuration or use default
  250. const USER_CONFIG = GM_getValue('commitLabelsConfig', DEFAULT_CONFIG);
  251. // Ensure backward compatibility with older versions
  252. if (USER_CONFIG.enableTooltips === undefined) {
  253. USER_CONFIG.enableTooltips = true;
  254. GM_setValue('commitLabelsConfig', USER_CONFIG);
  255. }
  256. // Ensure labelsVisible exists in config (for backward compatibility)
  257. if (USER_CONFIG.labelsVisible === undefined) {
  258. USER_CONFIG.labelsVisible = true;
  259. GM_setValue('commitLabelsConfig', USER_CONFIG);
  260. }
  261. // Make sure all commit types have descriptions (for backward compatibility)
  262. let configUpdated = false;
  263. Object.entries(USER_CONFIG.commitTypes).forEach(([type, config]) => {
  264. if (!config.description && DEFAULT_CONFIG.commitTypes[type]) {
  265. USER_CONFIG.commitTypes[type].description = DEFAULT_CONFIG.commitTypes[type].description;
  266. configUpdated = true;
  267. }
  268. });
  269. if (configUpdated) {
  270. GM_setValue('commitLabelsConfig', USER_CONFIG);
  271. }
  272.  
  273. // Create floating toggle button for labels
  274. function createLabelToggle() {
  275. // Only create if we're on a commit page
  276. if (!isCommitPage()) return;
  277. // Check if toggle already exists
  278. if (document.getElementById('commit-labels-toggle')) return;
  279. // Create a container for both buttons
  280. const buttonContainer = document.createElement('div');
  281. buttonContainer.id = 'commit-labels-buttons';
  282. buttonContainer.style.cssText = `
  283. position: fixed;
  284. bottom: 20px;
  285. right: 20px;
  286. display: flex;
  287. gap: 8px;
  288. z-index: 9999;
  289. `;
  290. // Label toggle button
  291. const toggleBtn = document.createElement('button');
  292. toggleBtn.id = 'commit-labels-toggle';
  293. toggleBtn.textContent = USER_CONFIG.labelsVisible ? '🏷️' : '🏷️';
  294. toggleBtn.title = USER_CONFIG.labelsVisible ? 'Hide commit labels' : 'Show commit labels';
  295. toggleBtn.style.cssText = `
  296. width: 32px;
  297. height: 32px;
  298. border-radius: 6px;
  299. background: rgba(31, 35, 40, 0.6);
  300. color: #adbac7;
  301. border: 1px solid rgba(205, 217, 229, 0.1);
  302. font-size: 14px;
  303. cursor: pointer;
  304. display: flex;
  305. align-items: center;
  306. justify-content: center;
  307. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  308. opacity: 0.5;
  309. transition: opacity 0.2s, transform 0.2s, background-color 0.2s;
  310. backdrop-filter: blur(4px);
  311. `;
  312. // Config toggle button
  313. const configBtn = document.createElement('button');
  314. configBtn.id = 'commit-labels-config-toggle';
  315. configBtn.textContent = '⚙️';
  316. configBtn.title = 'Open configuration';
  317. configBtn.style.cssText = `
  318. width: 32px;
  319. height: 32px;
  320. border-radius: 6px;
  321. background: rgba(31, 35, 40, 0.6);
  322. color: #adbac7;
  323. border: 1px solid rgba(205, 217, 229, 0.1);
  324. font-size: 14px;
  325. cursor: pointer;
  326. display: flex;
  327. align-items: center;
  328. justify-content: center;
  329. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  330. opacity: 0.5;
  331. transition: opacity 0.2s, transform 0.2s, background-color 0.2s;
  332. backdrop-filter: blur(4px);
  333. `;
  334. // Add hover effect for label toggle
  335. toggleBtn.addEventListener('mouseenter', () => {
  336. toggleBtn.style.opacity = '1';
  337. toggleBtn.style.background = currentTheme === 'light' ?
  338. 'rgba(246, 248, 250, 0.8)' : 'rgba(22, 27, 34, 0.8)';
  339. toggleBtn.style.color = currentTheme === 'light' ? '#24292f' : '#e6edf3';
  340. });
  341. toggleBtn.addEventListener('mouseleave', () => {
  342. toggleBtn.style.opacity = '0.5';
  343. toggleBtn.style.background = 'rgba(31, 35, 40, 0.6)';
  344. toggleBtn.style.color = '#adbac7';
  345. });
  346. // Add hover effect for config button
  347. configBtn.addEventListener('mouseenter', () => {
  348. configBtn.style.opacity = '1';
  349. configBtn.style.background = currentTheme === 'light' ?
  350. 'rgba(246, 248, 250, 0.8)' : 'rgba(22, 27, 34, 0.8)';
  351. configBtn.style.color = currentTheme === 'light' ? '#24292f' : '#e6edf3';
  352. });
  353. configBtn.addEventListener('mouseleave', () => {
  354. configBtn.style.opacity = '0.5';
  355. configBtn.style.background = 'rgba(31, 35, 40, 0.6)';
  356. configBtn.style.color = '#adbac7';
  357. });
  358. // Toggle labels on click
  359. toggleBtn.addEventListener('click', () => {
  360. USER_CONFIG.labelsVisible = !USER_CONFIG.labelsVisible;
  361. GM_setValue('commitLabelsConfig', USER_CONFIG);
  362. // Update button
  363. toggleBtn.textContent = USER_CONFIG.labelsVisible ? '🏷️' : '🏷️';
  364. toggleBtn.style.textDecoration = USER_CONFIG.labelsVisible ? 'none' : 'line-through';
  365. toggleBtn.title = USER_CONFIG.labelsVisible ? 'Hide commit labels' : 'Show commit labels';
  366. // Toggle label visibility
  367. document.querySelectorAll('.commit-label').forEach(label => {
  368. label.style.display = USER_CONFIG.labelsVisible ? 'inline-flex' : 'none';
  369. });
  370. });
  371. // Open config window on click
  372. configBtn.addEventListener('click', () => {
  373. createConfigWindow();
  374. });
  375. // Add buttons to container
  376. buttonContainer.appendChild(toggleBtn);
  377. buttonContainer.appendChild(configBtn);
  378. document.body.appendChild(buttonContainer);
  379. // Set initial state
  380. toggleBtn.style.textDecoration = USER_CONFIG.labelsVisible ? 'none' : 'line-through';
  381. }
  382.  
  383. // Create configuration window
  384. function createConfigWindow() {
  385. // Get current theme colors for the config window
  386. const isDark = isDarkTheme(currentTheme);
  387. const configStyles = {
  388. window: {
  389. background: isDark ? '#0d1117' : '#ffffff',
  390. border: isDark ? '1px solid #30363d' : '1px solid #d0d7de',
  391. color: isDark ? '#c9d1d9' : '#24292f',
  392. boxShadow: isDark ? '0 0 10px rgba(0,0,0,0.5)' : '0 0 10px rgba(0,0,0,0.2)'
  393. },
  394. button: {
  395. primary: {
  396. background: '#238636',
  397. color: '#ffffff',
  398. border: 'none'
  399. },
  400. secondary: {
  401. background: isDark ? '#21262d' : '#f6f8fa',
  402. color: isDark ? '#c9d1d9' : '#24292f',
  403. border: isDark ? '1px solid #30363d' : '1px solid #d0d7de'
  404. },
  405. danger: {
  406. background: isDark ? '#21262d' : '#f6f8fa',
  407. color: '#f85149',
  408. border: isDark ? '1px solid #30363d' : '1px solid #d0d7de'
  409. }
  410. },
  411. input: {
  412. background: isDark ? '#161b22' : '#f6f8fa',
  413. color: isDark ? '#c9d1d9' : '#24292f',
  414. border: isDark ? '1px solid #30363d' : '1px solid #d0d7de'
  415. },
  416. text: {
  417. dim: isDark ? '#8b949e' : '#6e7781',
  418. link: isDark ? '#58a6ff' : '#0969da'
  419. }
  420. };
  421. const configWindow = document.createElement('div');
  422. configWindow.style.cssText = `
  423. position: fixed;
  424. top: 50%;
  425. left: 50%;
  426. transform: translate(-50%, -50%);
  427. background: ${configStyles.window.background};
  428. border: ${configStyles.window.border};
  429. border-radius: 6px;
  430. padding: 20px;
  431. z-index: 9999;
  432. width: 600px;
  433. max-height: 80vh;
  434. overflow-y: auto;
  435. color: ${configStyles.window.color};
  436. box-shadow: ${configStyles.window.boxShadow};
  437. `;
  438.  
  439. // Header with title and repo link
  440. const titleContainer = document.createElement('div');
  441. titleContainer.style.display = 'flex';
  442. titleContainer.style.justifyContent = 'space-between';
  443. titleContainer.style.alignItems = 'center';
  444. titleContainer.style.marginBottom = '20px';
  445.  
  446. const title = document.createElement('h2');
  447. title.textContent = 'Commit Labels Configuration';
  448. title.style.margin = '0';
  449.  
  450. // Repository link container with profile pic
  451. const repoContainer = document.createElement('div');
  452. repoContainer.style.display = 'flex';
  453. repoContainer.style.alignItems = 'center';
  454. repoContainer.style.gap = '8px';
  455. // Owner profile picture
  456. const profilePic = document.createElement('img');
  457. profilePic.src = 'https://raw.githubusercontent.com/nazdridoy/nazhome/main/public/favicons/nazhome.svg';
  458. profilePic.alt = 'Owner';
  459. profilePic.style.cssText = `
  460. width: 36px;
  461. height: 36px;
  462. border-radius: 50%;
  463. background: ${isDark ? '#30363d' : '#eaeef2'};
  464. padding: 3px;
  465. vertical-align: middle;
  466. `;
  467. const repoLink = document.createElement('a');
  468. repoLink.href = 'https://github.com/nazdridoy/github-commit-labels';
  469. repoLink.target = '_blank';
  470. repoLink.textContent = 'GitHub Repository';
  471. repoLink.style.cssText = `
  472. color: ${configStyles.text.link};
  473. text-decoration: none;
  474. font-size: 15px;
  475. vertical-align: middle;
  476. `;
  477. repoLink.addEventListener('mouseenter', () => {
  478. repoLink.style.textDecoration = 'underline';
  479. });
  480. repoLink.addEventListener('mouseleave', () => {
  481. repoLink.style.textDecoration = 'none';
  482. });
  483.  
  484. repoContainer.appendChild(profilePic);
  485. repoContainer.appendChild(repoLink);
  486. titleContainer.appendChild(title);
  487. titleContainer.appendChild(repoContainer);
  488. configWindow.appendChild(titleContainer);
  489.  
  490. // Remove Prefix Option
  491. const prefixDiv = document.createElement('div');
  492. prefixDiv.style.marginBottom = '20px';
  493. const prefixCheckbox = document.createElement('input');
  494. prefixCheckbox.type = 'checkbox';
  495. prefixCheckbox.checked = USER_CONFIG.removePrefix;
  496. prefixCheckbox.id = 'remove-prefix';
  497. prefixCheckbox.style.marginRight = '5px';
  498. const prefixLabel = document.createElement('label');
  499. prefixLabel.htmlFor = 'remove-prefix';
  500. prefixLabel.textContent = 'Remove commit type prefix from message';
  501. prefixDiv.appendChild(prefixCheckbox);
  502. prefixDiv.appendChild(prefixLabel);
  503. configWindow.appendChild(prefixDiv);
  504.  
  505. // Add toggle for tooltips with preview
  506. const tooltipDiv = document.createElement('div');
  507. tooltipDiv.style.marginBottom = '20px';
  508. const tooltipHeader = document.createElement('div');
  509. tooltipHeader.style.display = 'flex';
  510. tooltipHeader.style.alignItems = 'center';
  511. tooltipHeader.style.marginBottom = '5px';
  512. const tooltipCheckbox = document.createElement('input');
  513. tooltipCheckbox.type = 'checkbox';
  514. tooltipCheckbox.checked = USER_CONFIG.enableTooltips;
  515. tooltipCheckbox.id = 'enable-tooltips';
  516. tooltipCheckbox.style.marginRight = '5px';
  517. const tooltipLabel = document.createElement('label');
  518. tooltipLabel.htmlFor = 'enable-tooltips';
  519. tooltipLabel.textContent = 'Enable tooltips with extended descriptions';
  520. tooltipLabel.style.marginRight = '15px';
  521. // Add tooltip preview
  522. const previewLabel = document.createElement('span');
  523. previewLabel.textContent = 'Preview: ';
  524. previewLabel.style.marginRight = '5px';
  525. const previewExample = document.createElement('span');
  526. previewExample.className = 'tooltip-preview-label';
  527. previewExample.innerHTML = '✨ <span>Feature</span>';
  528. previewExample.dataset.description = 'New user features (not for new files without user features)';
  529. previewExample.style.cssText = `
  530. display: inline-flex;
  531. align-items: center;
  532. justify-content: center;
  533. height: 24px;
  534. padding: 0 10px;
  535. border-radius: 20px;
  536. background: ${isDark ? 'rgba(35, 134, 54, 0.2)' : 'rgba(31, 136, 61, 0.1)'};
  537. color: ${isDark ? '#7ee787' : '#1a7f37'};
  538. cursor: help;
  539. `;
  540. tooltipHeader.appendChild(tooltipCheckbox);
  541. tooltipHeader.appendChild(tooltipLabel);
  542. tooltipHeader.appendChild(previewLabel);
  543. tooltipHeader.appendChild(previewExample);
  544. // Create custom preview tooltip
  545. previewExample.addEventListener('mouseenter', (e) => {
  546. if (!tooltipCheckbox.checked) return;
  547. const tooltipPreview = document.createElement('div');
  548. tooltipPreview.className = 'tooltip-preview';
  549. tooltipPreview.textContent = previewExample.dataset.description;
  550. const rect = e.target.getBoundingClientRect();
  551. tooltipPreview.style.cssText = `
  552. position: fixed;
  553. top: ${rect.bottom + 5}px;
  554. left: ${rect.left}px;
  555. max-width: 300px;
  556. padding: 8px 12px;
  557. color: ${isDark ? '#e6edf3' : '#ffffff'};
  558. text-align: center;
  559. background-color: ${isDark ? '#161b22' : '#24292f'};
  560. border-radius: 6px;
  561. border: ${isDark ? '1px solid #30363d' : '1px solid #d0d7de'};
  562. box-shadow: 0 3px 12px rgba(0,0,0,0.4);
  563. font-size: 12px;
  564. z-index: 10000;
  565. pointer-events: none;
  566. `;
  567. document.body.appendChild(tooltipPreview);
  568. });
  569. previewExample.addEventListener('mouseleave', () => {
  570. const tooltipPreview = document.querySelector('.tooltip-preview');
  571. if (tooltipPreview) {
  572. document.body.removeChild(tooltipPreview);
  573. }
  574. });
  575. tooltipDiv.appendChild(tooltipHeader);
  576. // Add explanation text
  577. const tooltipExplanation = document.createElement('div');
  578. tooltipExplanation.textContent = 'Tooltips show detailed descriptions when hovering over commit labels.';
  579. tooltipExplanation.style.color = configStyles.text.dim;
  580. tooltipExplanation.style.fontSize = '12px';
  581. tooltipExplanation.style.marginTop = '5px';
  582. tooltipDiv.appendChild(tooltipExplanation);
  583. configWindow.insertBefore(tooltipDiv, prefixDiv.nextSibling);
  584.  
  585. // After prefixDiv and tooltipDiv, add a toggle for showing the floating button
  586. const floatingBtnDiv = document.createElement('div');
  587. floatingBtnDiv.style.marginBottom = '20px';
  588. // Add showFloatingButton to USER_CONFIG if it doesn't exist
  589. if (USER_CONFIG.showFloatingButton === undefined) {
  590. USER_CONFIG.showFloatingButton = true;
  591. GM_setValue('commitLabelsConfig', USER_CONFIG);
  592. }
  593. const floatingBtnCheckbox = document.createElement('input');
  594. floatingBtnCheckbox.type = 'checkbox';
  595. floatingBtnCheckbox.checked = USER_CONFIG.showFloatingButton;
  596. floatingBtnCheckbox.id = 'show-floating-btn';
  597. floatingBtnCheckbox.style.marginRight = '5px';
  598. const floatingBtnLabel = document.createElement('label');
  599. floatingBtnLabel.htmlFor = 'show-floating-btn';
  600. floatingBtnLabel.textContent = 'Show floating toggle button';
  601. floatingBtnDiv.appendChild(floatingBtnCheckbox);
  602. floatingBtnDiv.appendChild(floatingBtnLabel);
  603. configWindow.insertBefore(floatingBtnDiv, tooltipDiv.nextSibling);
  604.  
  605. // After the tooltipDiv and before the floatingBtnDiv in the createConfigWindow function:
  606. const scopeDiv = document.createElement('div');
  607. scopeDiv.style.marginBottom = '20px';
  608. const scopeCheckbox = document.createElement('input');
  609. scopeCheckbox.type = 'checkbox';
  610. scopeCheckbox.checked = USER_CONFIG.showScope;
  611. scopeCheckbox.id = 'show-scope';
  612. scopeCheckbox.style.marginRight = '5px';
  613. const scopeLabel = document.createElement('label');
  614. scopeLabel.htmlFor = 'show-scope';
  615. scopeLabel.textContent = 'Show commit scope in labels (e.g., "feat(api): message" shows "api" in label)';
  616. scopeDiv.appendChild(scopeCheckbox);
  617. scopeDiv.appendChild(scopeLabel);
  618. configWindow.insertBefore(scopeDiv, floatingBtnDiv.nextSibling);
  619.  
  620. // Add debug mode toggle
  621. const debugDiv = document.createElement('div');
  622. debugDiv.style.marginBottom = '20px';
  623. const debugCheckbox = document.createElement('input');
  624. debugCheckbox.type = 'checkbox';
  625. debugCheckbox.checked = USER_CONFIG.debugMode;
  626. debugCheckbox.id = 'debug-mode';
  627. debugCheckbox.style.marginRight = '5px';
  628. const debugLabel = document.createElement('label');
  629. debugLabel.htmlFor = 'debug-mode';
  630. debugLabel.textContent = 'Enable debug mode (shows detailed logs in console)';
  631. debugDiv.appendChild(debugCheckbox);
  632. debugDiv.appendChild(debugLabel);
  633. configWindow.insertBefore(debugDiv, prefixDiv.nextSibling);
  634.  
  635. // Commit Types Configuration
  636. const typesContainer = document.createElement('div');
  637. typesContainer.style.marginBottom = '20px';
  638.  
  639. // Group commit types by their label
  640. const groupedTypes = {};
  641. Object.entries(USER_CONFIG.commitTypes).forEach(([type, config]) => {
  642. const key = config.label;
  643. if (!groupedTypes[key]) {
  644. groupedTypes[key] = {
  645. types: [],
  646. config: config
  647. };
  648. }
  649. groupedTypes[key].types.push(type);
  650. });
  651.  
  652. // Create rows for grouped types
  653. Object.entries(groupedTypes).forEach(([label, { types, config }]) => {
  654. const typeDiv = document.createElement('div');
  655. typeDiv.style.marginBottom = '10px';
  656. typeDiv.style.display = 'flex';
  657. typeDiv.style.alignItems = 'center';
  658. typeDiv.style.gap = '10px';
  659.  
  660. // Type names (with aliases) and edit button container
  661. const typeContainer = document.createElement('div');
  662. typeContainer.style.display = 'flex';
  663. typeContainer.style.width = '150px';
  664. typeContainer.style.alignItems = 'center';
  665. typeContainer.style.gap = '4px';
  666.  
  667. const typeSpan = document.createElement('span');
  668. typeSpan.style.color = configStyles.text.dim;
  669. typeSpan.style.flex = '1';
  670. typeSpan.textContent = types.join(', ') + ':';
  671.  
  672. const editAliasButton = document.createElement('button');
  673. editAliasButton.textContent = '✏️';
  674. editAliasButton.title = 'Edit Aliases';
  675. editAliasButton.style.cssText = `
  676. padding: 2px 4px;
  677. background: ${configStyles.button.secondary.background};
  678. color: ${isDark ? '#58a6ff' : '#0969da'};
  679. border: ${configStyles.button.secondary.border};
  680. border-radius: 4px;
  681. cursor: pointer;
  682. font-size: 10px;
  683. `;
  684.  
  685. editAliasButton.onclick = () => {
  686. const currentAliases = types.join(', ');
  687. const newAliases = prompt('Edit aliases (separate with commas):', currentAliases);
  688.  
  689. if (newAliases && newAliases.trim()) {
  690. const newTypes = newAliases.split(',').map(t => t.trim().toLowerCase()).filter(t => t);
  691.  
  692. // Check if any new aliases conflict with other types
  693. const conflictingType = newTypes.find(type =>
  694. USER_CONFIG.commitTypes[type] && !types.includes(type)
  695. );
  696.  
  697. if (conflictingType) {
  698. alert(`The alias "${conflictingType}" already exists in another group!`);
  699. return;
  700. }
  701.  
  702. // Remove old types
  703. types.forEach(type => delete USER_CONFIG.commitTypes[type]);
  704.  
  705. // Add new types with same config
  706. newTypes.forEach(type => {
  707. USER_CONFIG.commitTypes[type] = { ...config };
  708. });
  709.  
  710. // Update the display
  711. typeSpan.textContent = newTypes.join(', ') + ':';
  712.  
  713. // Update dataset for inputs
  714. const inputs = typeDiv.querySelectorAll('input, select');
  715. inputs.forEach(input => {
  716. input.dataset.types = newTypes.join(',');
  717. });
  718. }
  719. };
  720.  
  721. typeContainer.appendChild(typeSpan);
  722. typeContainer.appendChild(editAliasButton);
  723. typeDiv.appendChild(typeContainer);
  724.  
  725. // Emoji input
  726. const emojiInput = document.createElement('input');
  727. emojiInput.type = 'text';
  728. emojiInput.value = config.emoji;
  729. emojiInput.style.width = '40px';
  730. emojiInput.style.background = configStyles.input.background;
  731. emojiInput.style.color = configStyles.input.color;
  732. emojiInput.style.border = configStyles.input.border;
  733. emojiInput.style.borderRadius = '4px';
  734. emojiInput.style.padding = '4px';
  735. emojiInput.dataset.types = types.join(',');
  736. emojiInput.dataset.field = 'emoji';
  737. typeDiv.appendChild(emojiInput);
  738.  
  739. // Label input
  740. const labelInput = document.createElement('input');
  741. labelInput.type = 'text';
  742. labelInput.value = config.label;
  743. labelInput.style.width = '120px';
  744. labelInput.style.background = configStyles.input.background;
  745. labelInput.style.color = configStyles.input.color;
  746. labelInput.style.border = configStyles.input.border;
  747. labelInput.style.borderRadius = '4px';
  748. labelInput.style.padding = '4px';
  749. labelInput.dataset.types = types.join(',');
  750. labelInput.dataset.field = 'label';
  751. typeDiv.appendChild(labelInput);
  752.  
  753. // Color select
  754. const colorSelect = document.createElement('select');
  755. Object.keys(COLORS).forEach(color => {
  756. const option = document.createElement('option');
  757. option.value = color;
  758. option.textContent = color;
  759. if (config.color === color) option.selected = true;
  760. colorSelect.appendChild(option);
  761. });
  762. colorSelect.style.background = configStyles.input.background;
  763. colorSelect.style.color = configStyles.input.color;
  764. colorSelect.style.border = configStyles.input.border;
  765. colorSelect.style.borderRadius = '4px';
  766. colorSelect.style.padding = '4px';
  767. colorSelect.dataset.types = types.join(',');
  768. colorSelect.dataset.field = 'color';
  769. typeDiv.appendChild(colorSelect);
  770.  
  771. // Delete button
  772. const deleteButton = document.createElement('button');
  773. deleteButton.textContent = '🗑️';
  774. deleteButton.style.cssText = `
  775. padding: 2px 8px;
  776. background: ${configStyles.button.danger.background};
  777. color: ${configStyles.button.danger.color};
  778. border: ${configStyles.button.danger.border};
  779. border-radius: 4px;
  780. cursor: pointer;
  781. `;
  782. deleteButton.onclick = () => {
  783. if (confirm(`Delete commit types "${types.join(', ')}"?`)) {
  784. typeDiv.remove();
  785. types.forEach(type => delete USER_CONFIG.commitTypes[type]);
  786. }
  787. };
  788. typeDiv.appendChild(deleteButton);
  789.  
  790. typesContainer.appendChild(typeDiv);
  791. });
  792.  
  793. // Add "Add New Type" button
  794. const addNewButton = document.createElement('button');
  795. addNewButton.textContent = '+ Add New Type';
  796. addNewButton.style.cssText = `
  797. margin-bottom: 15px;
  798. padding: 5px 16px;
  799. background: ${configStyles.button.primary.background};
  800. color: ${configStyles.button.primary.color};
  801. border: ${configStyles.button.primary.border};
  802. border-radius: 6px;
  803. cursor: pointer;
  804. `;
  805.  
  806. addNewButton.onclick = () => {
  807. const typeInput = prompt('Enter the commit type and aliases (separated by commas, e.g., "added, add"):', '');
  808. if (typeInput && typeInput.trim()) {
  809. const types = typeInput.split(',').map(t => t.trim().toLowerCase()).filter(t => t);
  810.  
  811. // Check if any of the types already exist
  812. const existingType = types.find(type => USER_CONFIG.commitTypes[type]);
  813. if (existingType) {
  814. alert(`The commit type "${existingType}" already exists!`);
  815. return;
  816. }
  817.  
  818. // Create base config for all aliases
  819. const baseConfig = {
  820. emoji: '🔄',
  821. label: types[0].charAt(0).toUpperCase() + types[0].slice(1),
  822. color: 'blue',
  823. description: 'Custom commit type'
  824. };
  825.  
  826. // Add all types to config with the same settings
  827. types.forEach(type => {
  828. USER_CONFIG.commitTypes[type] = { ...baseConfig };
  829. });
  830.  
  831. // Create and add new type row
  832. const typeDiv = document.createElement('div');
  833. typeDiv.style.marginBottom = '10px';
  834. typeDiv.style.display = 'flex';
  835. typeDiv.style.alignItems = 'center';
  836. typeDiv.style.gap = '10px';
  837.  
  838. // Type names (with aliases)
  839. const typeSpan = document.createElement('span');
  840. typeSpan.style.width = '150px';
  841. typeSpan.style.color = configStyles.text.dim;
  842. typeSpan.textContent = types.join(', ') + ':';
  843. typeDiv.appendChild(typeSpan);
  844.  
  845. // Emoji input
  846. const emojiInput = document.createElement('input');
  847. emojiInput.type = 'text';
  848. emojiInput.value = baseConfig.emoji;
  849. emojiInput.style.width = '40px';
  850. emojiInput.style.background = configStyles.input.background;
  851. emojiInput.style.color = configStyles.input.color;
  852. emojiInput.style.border = configStyles.input.border;
  853. emojiInput.style.borderRadius = '4px';
  854. emojiInput.style.padding = '4px';
  855. emojiInput.dataset.types = types.join(',');
  856. emojiInput.dataset.field = 'emoji';
  857. typeDiv.appendChild(emojiInput);
  858.  
  859. // Label input
  860. const labelInput = document.createElement('input');
  861. labelInput.type = 'text';
  862. labelInput.value = baseConfig.label;
  863. labelInput.style.width = '120px';
  864. labelInput.style.background = configStyles.input.background;
  865. labelInput.style.color = configStyles.input.color;
  866. labelInput.style.border = configStyles.input.border;
  867. labelInput.style.borderRadius = '4px';
  868. labelInput.style.padding = '4px';
  869. labelInput.dataset.types = types.join(',');
  870. labelInput.dataset.field = 'label';
  871. typeDiv.appendChild(labelInput);
  872.  
  873. // Color select
  874. const colorSelect = document.createElement('select');
  875. Object.keys(COLORS).forEach(color => {
  876. const option = document.createElement('option');
  877. option.value = color;
  878. option.textContent = color;
  879. if (color === 'blue') option.selected = true;
  880. colorSelect.appendChild(option);
  881. });
  882. colorSelect.style.background = configStyles.input.background;
  883. colorSelect.style.color = configStyles.input.color;
  884. colorSelect.style.border = configStyles.input.border;
  885. colorSelect.style.borderRadius = '4px';
  886. colorSelect.style.padding = '4px';
  887. colorSelect.dataset.types = types.join(',');
  888. colorSelect.dataset.field = 'color';
  889. typeDiv.appendChild(colorSelect);
  890.  
  891. // Delete button
  892. const deleteButton = document.createElement('button');
  893. deleteButton.textContent = '🗑️';
  894. deleteButton.style.cssText = `
  895. padding: 2px 8px;
  896. background: ${configStyles.button.danger.background};
  897. color: ${configStyles.button.danger.color};
  898. border: ${configStyles.button.danger.border};
  899. border-radius: 4px;
  900. cursor: pointer;
  901. `;
  902. deleteButton.onclick = () => {
  903. if (confirm(`Delete commit types "${types.join(', ')}"?`)) {
  904. typeDiv.remove();
  905. types.forEach(type => delete USER_CONFIG.commitTypes[type]);
  906. }
  907. };
  908. typeDiv.appendChild(deleteButton);
  909.  
  910. typesContainer.appendChild(typeDiv);
  911. }
  912. };
  913.  
  914. configWindow.appendChild(addNewButton);
  915. configWindow.appendChild(typesContainer);
  916.  
  917. // Save and Close buttons
  918. const buttonContainer = document.createElement('div');
  919. buttonContainer.style.display = 'flex';
  920. buttonContainer.style.gap = '10px';
  921. buttonContainer.style.justifyContent = 'flex-end';
  922.  
  923. const saveButton = document.createElement('button');
  924. saveButton.textContent = 'Save';
  925. saveButton.style.cssText = `
  926. padding: 5px 16px;
  927. background: ${configStyles.button.primary.background};
  928. color: ${configStyles.button.primary.color};
  929. border: ${configStyles.button.primary.border};
  930. border-radius: 6px;
  931. cursor: pointer;
  932. `;
  933.  
  934. const closeButton = document.createElement('button');
  935. closeButton.textContent = 'Close';
  936. closeButton.style.cssText = `
  937. padding: 5px 16px;
  938. background: ${configStyles.button.secondary.background};
  939. color: ${configStyles.button.secondary.color};
  940. border: ${configStyles.button.secondary.border};
  941. border-radius: 6px;
  942. cursor: pointer;
  943. `;
  944.  
  945. // Add Reset button next to Save and Close
  946. const resetButton = document.createElement('button');
  947. resetButton.textContent = 'Reset to Default';
  948. resetButton.style.cssText = `
  949. padding: 5px 16px;
  950. background: ${configStyles.button.danger.background};
  951. color: ${configStyles.button.danger.color};
  952. border: ${configStyles.button.danger.border};
  953. border-radius: 6px;
  954. cursor: pointer;
  955. margin-right: auto; // This pushes Save/Close to the right
  956. `;
  957.  
  958. resetButton.onclick = () => {
  959. if (confirm('Are you sure you want to reset all settings to default? This will remove all custom types and settings.')) {
  960. GM_setValue('commitLabelsConfig', DEFAULT_CONFIG);
  961. location.reload();
  962. }
  963. };
  964.  
  965. saveButton.onclick = () => {
  966. const newConfig = { ...USER_CONFIG };
  967. newConfig.removePrefix = prefixCheckbox.checked;
  968. newConfig.enableTooltips = tooltipCheckbox.checked;
  969. newConfig.showFloatingButton = floatingBtnCheckbox.checked;
  970. newConfig.showScope = scopeCheckbox.checked;
  971. newConfig.debugMode = debugCheckbox.checked; // Add debug mode
  972. newConfig.commitTypes = {};
  973.  
  974. typesContainer.querySelectorAll('input, select').forEach(input => {
  975. const types = input.dataset.types.split(',');
  976. const field = input.dataset.field;
  977.  
  978. types.forEach(type => {
  979. if (!newConfig.commitTypes[type]) {
  980. newConfig.commitTypes[type] = {};
  981. }
  982. newConfig.commitTypes[type][field] = input.value;
  983. });
  984. });
  985.  
  986. GM_setValue('commitLabelsConfig', newConfig);
  987. location.reload();
  988. };
  989.  
  990. closeButton.onclick = () => {
  991. document.body.removeChild(configWindow);
  992. };
  993.  
  994. buttonContainer.appendChild(resetButton);
  995. buttonContainer.appendChild(closeButton);
  996. buttonContainer.appendChild(saveButton);
  997. configWindow.appendChild(buttonContainer);
  998.  
  999. document.body.appendChild(configWindow);
  1000. }
  1001.  
  1002. // Create export/import dialog
  1003. function createExportImportDialog() {
  1004. // Check if dialog already exists
  1005. if (document.getElementById('config-export-import')) {
  1006. document.getElementById('config-export-import').remove();
  1007. }
  1008. const dialog = document.createElement('div');
  1009. dialog.id = 'config-export-import';
  1010. dialog.style.cssText = `
  1011. position: fixed;
  1012. top: 50%;
  1013. left: 50%;
  1014. transform: translate(-50%, -50%);
  1015. background: #0d1117;
  1016. border: 1px solid #30363d;
  1017. border-radius: 6px;
  1018. padding: 20px;
  1019. z-index: 9999;
  1020. width: 500px;
  1021. max-height: 80vh;
  1022. overflow-y: auto;
  1023. color: #c9d1d9;
  1024. box-shadow: 0 0 20px rgba(0,0,0,0.7);
  1025. `;
  1026. const title = document.createElement('h2');
  1027. title.textContent = 'Export/Import Configuration';
  1028. title.style.marginBottom = '15px';
  1029. const exportSection = document.createElement('div');
  1030. exportSection.style.marginBottom = '20px';
  1031. const exportTitle = document.createElement('h3');
  1032. exportTitle.textContent = 'Export Configuration';
  1033. exportTitle.style.marginBottom = '10px';
  1034. const configOutput = document.createElement('textarea');
  1035. configOutput.readOnly = true;
  1036. configOutput.value = JSON.stringify(USER_CONFIG, null, 2);
  1037. configOutput.style.cssText = `
  1038. width: 100%;
  1039. height: 150px;
  1040. background: #161b22;
  1041. color: #c9d1d9;
  1042. border: 1px solid #30363d;
  1043. border-radius: 6px;
  1044. padding: 10px;
  1045. font-family: monospace;
  1046. resize: vertical;
  1047. margin-bottom: 10px;
  1048. `;
  1049. const copyButton = document.createElement('button');
  1050. copyButton.textContent = 'Copy to Clipboard';
  1051. copyButton.style.cssText = `
  1052. padding: 6px 16px;
  1053. background: #238636;
  1054. color: #fff;
  1055. border: none;
  1056. border-radius: 6px;
  1057. cursor: pointer;
  1058. margin-right: 10px;
  1059. `;
  1060. copyButton.onclick = () => {
  1061. configOutput.select();
  1062. document.execCommand('copy');
  1063. copyButton.textContent = 'Copied!';
  1064. setTimeout(() => {
  1065. copyButton.textContent = 'Copy to Clipboard';
  1066. }, 2000);
  1067. };
  1068. exportSection.appendChild(exportTitle);
  1069. exportSection.appendChild(configOutput);
  1070. exportSection.appendChild(copyButton);
  1071. const importSection = document.createElement('div');
  1072. importSection.style.marginBottom = '20px';
  1073. const importTitle = document.createElement('h3');
  1074. importTitle.textContent = 'Import Configuration';
  1075. importTitle.style.marginBottom = '10px';
  1076. const configInput = document.createElement('textarea');
  1077. configInput.placeholder = 'Paste configuration JSON here...';
  1078. configInput.style.cssText = `
  1079. width: 100%;
  1080. height: 150px;
  1081. background: #161b22;
  1082. color: #c9d1d9;
  1083. border: 1px solid #30363d;
  1084. border-radius: 6px;
  1085. padding: 10px;
  1086. font-family: monospace;
  1087. resize: vertical;
  1088. margin-bottom: 10px;
  1089. `;
  1090. const importButton = document.createElement('button');
  1091. importButton.textContent = 'Import';
  1092. importButton.style.cssText = `
  1093. padding: 6px 16px;
  1094. background: #238636;
  1095. color: #fff;
  1096. border: none;
  1097. border-radius: 6px;
  1098. cursor: pointer;
  1099. margin-right: 10px;
  1100. `;
  1101. importButton.onclick = () => {
  1102. try {
  1103. const newConfig = JSON.parse(configInput.value);
  1104. // Validate basic structure
  1105. if (!newConfig.commitTypes) {
  1106. throw new Error('Invalid configuration: missing commitTypes object');
  1107. }
  1108. if (confirm('Are you sure you want to import this configuration? This will overwrite your current settings.')) {
  1109. GM_setValue('commitLabelsConfig', newConfig);
  1110. alert('Configuration imported successfully! Page will reload to apply changes.');
  1111. location.reload();
  1112. }
  1113. } catch (error) {
  1114. alert('Error importing configuration: ' + error.message);
  1115. }
  1116. };
  1117. const closeButton = document.createElement('button');
  1118. closeButton.textContent = 'Close';
  1119. closeButton.style.cssText = `
  1120. padding: 6px 16px;
  1121. background: #21262d;
  1122. color: #c9d1d9;
  1123. border: 1px solid #30363d;
  1124. border-radius: 6px;
  1125. cursor: pointer;
  1126. `;
  1127. closeButton.onclick = () => {
  1128. document.body.removeChild(dialog);
  1129. };
  1130. importSection.appendChild(importTitle);
  1131. importSection.appendChild(configInput);
  1132. importSection.appendChild(importButton);
  1133. dialog.appendChild(title);
  1134. dialog.appendChild(exportSection);
  1135. dialog.appendChild(importSection);
  1136. dialog.appendChild(closeButton);
  1137. document.body.appendChild(dialog);
  1138. }
  1139.  
  1140. // Register configuration menu command
  1141. GM_registerMenuCommand('Configure Commit Labels', createConfigWindow);
  1142. GM_registerMenuCommand('Toggle Labels', () => {
  1143. USER_CONFIG.labelsVisible = !USER_CONFIG.labelsVisible;
  1144. GM_setValue('commitLabelsConfig', USER_CONFIG);
  1145. // Toggle label visibility
  1146. document.querySelectorAll('.commit-label').forEach(label => {
  1147. label.style.display = USER_CONFIG.labelsVisible ? 'inline-flex' : 'none';
  1148. });
  1149. // Update toggle button if it exists
  1150. const toggleBtn = document.getElementById('commit-labels-toggle');
  1151. if (toggleBtn) {
  1152. toggleBtn.textContent = USER_CONFIG.labelsVisible ? '🏷️' : '🏷️';
  1153. toggleBtn.style.textDecoration = USER_CONFIG.labelsVisible ? 'none' : 'line-through';
  1154. toggleBtn.title = USER_CONFIG.labelsVisible ? 'Hide commit labels' : 'Show commit labels';
  1155. }
  1156. });
  1157. GM_registerMenuCommand('Export/Import Config', createExportImportDialog);
  1158.  
  1159. // Check if we're on a commit page
  1160. function isCommitPage() {
  1161. return window.location.pathname.includes('/commits') ||
  1162. window.location.pathname.includes('/commit/');
  1163. }
  1164.  
  1165. // Update colors when theme changes
  1166. function updateThemeColors() {
  1167. const newTheme = detectTheme();
  1168. if (newTheme !== currentTheme) {
  1169. currentTheme = newTheme;
  1170. // Map theme variants to our base themes for colors
  1171. let baseTheme = newTheme;
  1172. if (newTheme.startsWith('light_')) {
  1173. baseTheme = 'light';
  1174. } else if (newTheme.startsWith('dark_') && newTheme !== 'dark_dimmed') {
  1175. baseTheme = 'dark';
  1176. }
  1177. COLORS = THEME_COLORS[baseTheme] || THEME_COLORS.light;
  1178. // Update existing labels
  1179. document.querySelectorAll('.commit-label').forEach(label => {
  1180. const type = label.dataset.commitType;
  1181. if (type && USER_CONFIG.commitTypes[type]) {
  1182. const color = COLORS[USER_CONFIG.commitTypes[type].color];
  1183. if (color) {
  1184. label.style.backgroundColor = color.bg;
  1185. label.style.color = color.text;
  1186. }
  1187. }
  1188. });
  1189. }
  1190. }
  1191.  
  1192. // Debug logging function
  1193. function debugLog(message, data = null) {
  1194. if (USER_CONFIG.debugMode) {
  1195. const timestamp = new Date().toISOString();
  1196. const logMessage = `[GitHub Commit Labels Debug] [${timestamp}] ${message}`;
  1197. console.log(logMessage);
  1198. if (data) {
  1199. console.log('Data:', data);
  1200. }
  1201. }
  1202. }
  1203.  
  1204. // Helper function to safely query elements
  1205. function safeQuerySelector(selector) {
  1206. try {
  1207. const elements = document.querySelectorAll(selector);
  1208. debugLog(`Query selector "${selector}" found ${elements.length} elements`);
  1209. return elements;
  1210. } catch (error) {
  1211. debugLog(`Selector error for "${selector}":`, error);
  1212. return [];
  1213. }
  1214. }
  1215.  
  1216. // Debounce function to limit how often a function can be called
  1217. function debounce(func, wait) {
  1218. let timeout;
  1219. return function executedFunction(...args) {
  1220. const later = () => {
  1221. clearTimeout(timeout);
  1222. func(...args);
  1223. };
  1224. clearTimeout(timeout);
  1225. timeout = setTimeout(later, wait);
  1226. };
  1227. }
  1228.  
  1229. // Main function to add labels to commits
  1230. function addCommitLabels() {
  1231. debugLog('Starting addCommitLabels');
  1232. // Only proceed if we're on a commit page
  1233. if (!isCommitPage()) {
  1234. debugLog('Not on a commit page, exiting');
  1235. return;
  1236. }
  1237.  
  1238. debugLog('Updating theme colors');
  1239. updateThemeColors();
  1240. // Create toggle button if it doesn't exist and is enabled
  1241. if (USER_CONFIG.showFloatingButton !== false) {
  1242. debugLog('Creating label toggle button');
  1243. createLabelToggle();
  1244. }
  1245.  
  1246. // Try multiple selectors in order of reliability
  1247. const selectors = [
  1248. 'li[data-testid="commit-row-item"] h4 a[data-pjax="true"]', // Most reliable
  1249. '.Title-module__heading--upUxW a[data-pjax="true"]', // Backup
  1250. '.markdown-title a[data-pjax="true"]' // Legacy
  1251. ];
  1252.  
  1253. debugLog('Trying selectors:', selectors);
  1254. let commitMessages = [];
  1255. for (const selector of selectors) {
  1256. commitMessages = safeQuerySelector(selector);
  1257. if (commitMessages.length > 0) {
  1258. debugLog(`Using selector: ${selector}`);
  1259. break;
  1260. }
  1261. }
  1262.  
  1263. debugLog(`Found ${commitMessages.length} commit messages to process`);
  1264.  
  1265. // Debounce and batch process for performance improvement
  1266. let processedCount = 0;
  1267. const batchSize = 20;
  1268. const commitMessagesArray = Array.from(commitMessages);
  1269. const processCommitBatch = (startIndex) => {
  1270. debugLog(`Processing batch starting at index ${startIndex}`);
  1271. const endIndex = Math.min(startIndex + batchSize, commitMessagesArray.length);
  1272. for (let i = startIndex; i < endIndex; i++) {
  1273. try {
  1274. const message = commitMessagesArray[i];
  1275. const text = message.textContent.trim();
  1276. debugLog(`Processing commit message: ${text}`);
  1277. // Skip if this commit already has a label
  1278. if (message.parentElement.querySelector('.commit-label')) {
  1279. debugLog('Commit already has a label, skipping');
  1280. continue;
  1281. }
  1282.  
  1283. // Step 1: Basic parse - Capture type, middle part, and message
  1284. const basicMatch = text.match(/^(\w+)(.*?):\s*(.*)$/);
  1285. debugLog('Basic commit message match result:', basicMatch);
  1286.  
  1287. if (basicMatch) {
  1288. const type = basicMatch[1].toLowerCase();
  1289. const middlePart = basicMatch[2].trim();
  1290. const restOfMessage = basicMatch[3];
  1291.  
  1292. let scope = '';
  1293. let isBreaking = false;
  1294. let scopePart = middlePart; // Start with the full middle part
  1295.  
  1296. // Step 2: Check for breaking change indicator anywhere in the middle part
  1297. if (middlePart.includes('!')) {
  1298. isBreaking = true;
  1299. scopePart = middlePart.replace('!', '').trim(); // Remove ! for scope extraction
  1300. }
  1301. // Step 3: Check if the remaining part is a scope
  1302. if (scopePart.startsWith('(') && scopePart.endsWith(')')) {
  1303. scope = scopePart.slice(1, -1); // Extract scope content
  1304. }
  1305.  
  1306. debugLog(`Extracted: type=${type}, scope=${scope}, isBreaking=${isBreaking}, message=${restOfMessage}`);
  1307.  
  1308. if (USER_CONFIG.commitTypes[type]) {
  1309. debugLog(`Found matching commit type: ${type}`);
  1310. // Only add label if it hasn't been added yet
  1311. if (!message.parentElement.querySelector('.commit-label')) {
  1312. debugLog('Creating new label');
  1313. const label = document.createElement('span');
  1314. label.className = 'commit-label';
  1315. label.dataset.commitType = type;
  1316. label.dataset.commitScope = scope;
  1317. if (isBreaking) {
  1318. label.dataset.isBreaking = 'true';
  1319. }
  1320. const color = COLORS[USER_CONFIG.commitTypes[type].color];
  1321. // Apply styles
  1322. const styles = {
  1323. ...USER_CONFIG.labelStyle,
  1324. backgroundColor: color.bg,
  1325. color: color.text,
  1326. display: USER_CONFIG.labelsVisible ? 'inline-flex' : 'none'
  1327. };
  1328.  
  1329. if (isBreaking) {
  1330. styles.border = '2px solid #d73a49';
  1331. styles.fontWeight = 'bold';
  1332. }
  1333.  
  1334. label.style.cssText = Object.entries(styles)
  1335. .map(([key, value]) => `${key.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${value}`)
  1336. .join(';');
  1337.  
  1338. // Enhanced tooltip
  1339. if (USER_CONFIG.enableTooltips && USER_CONFIG.commitTypes[type].description) {
  1340. // Store description in data attribute instead of title to avoid double tooltips
  1341. const description = USER_CONFIG.commitTypes[type].description;
  1342. const tooltipText = scope ?
  1343. `${description} (Scope: ${scope})` :
  1344. description;
  1345. label.dataset.description = tooltipText;
  1346. label.setAttribute('aria-label', tooltipText);
  1347. // Add tooltip indicator
  1348. label.style.cursor = 'help';
  1349. // For better accessibility
  1350. label.setAttribute('role', 'tooltip');
  1351. // Create a custom tooltip implementation if needed
  1352. label.addEventListener('mouseenter', (e) => {
  1353. // Check if we already have a custom tooltip showing
  1354. if (document.querySelector('.commit-label-tooltip')) {
  1355. return;
  1356. }
  1357. label.style.transform = 'translateY(-1px)';
  1358. label.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)';
  1359. // Force show tooltip by creating a custom one
  1360. if (label.dataset.description) {
  1361. const tooltip = document.createElement('div');
  1362. tooltip.className = 'commit-label-tooltip';
  1363. tooltip.textContent = label.dataset.description;
  1364. // Calculate position relative to viewport
  1365. const rect = e.target.getBoundingClientRect();
  1366. const top = rect.bottom + 5;
  1367. const left = rect.left;
  1368. tooltip.style.cssText = `
  1369. position: fixed;
  1370. top: ${top}px;
  1371. left: ${left}px;
  1372. max-width: 300px;
  1373. padding: 8px 12px;
  1374. color: #e6edf3;
  1375. text-align: center;
  1376. background-color: #161b22;
  1377. border-radius: 6px;
  1378. border: 1px solid #30363d;
  1379. box-shadow: 0 3px 12px rgba(0,0,0,0.4);
  1380. font-size: 12px;
  1381. z-index: 1000;
  1382. pointer-events: none;
  1383. `;
  1384. document.body.appendChild(tooltip);
  1385. // Adjust position if tooltip goes off-screen
  1386. const tooltipRect = tooltip.getBoundingClientRect();
  1387. if (tooltipRect.right > window.innerWidth) {
  1388. tooltip.style.left = `${window.innerWidth - tooltipRect.width - 10}px`;
  1389. }
  1390. }
  1391. });
  1392.  
  1393. label.addEventListener('mouseleave', () => {
  1394. label.style.transform = 'translateY(0)';
  1395. label.style.boxShadow = styles.boxShadow;
  1396. // Remove custom tooltip if it exists
  1397. const tooltip = document.querySelector('.commit-label-tooltip');
  1398. if (tooltip) {
  1399. document.body.removeChild(tooltip);
  1400. }
  1401. });
  1402. } else {
  1403. // Normal hover effect if tooltips are disabled
  1404. if (USER_CONFIG.commitTypes[type].description) {
  1405. label.title = USER_CONFIG.commitTypes[type].description;
  1406. }
  1407. label.addEventListener('mouseenter', () => {
  1408. label.style.transform = 'translateY(-1px)';
  1409. label.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)';
  1410. });
  1411.  
  1412. label.addEventListener('mouseleave', () => {
  1413. label.style.transform = 'translateY(0)';
  1414. label.style.boxShadow = styles.boxShadow;
  1415. });
  1416. }
  1417.  
  1418. const emoji = document.createElement('span');
  1419. emoji.style.marginRight = '4px';
  1420. emoji.style.fontSize = '14px';
  1421. emoji.style.lineHeight = '1';
  1422. emoji.textContent = USER_CONFIG.commitTypes[type].emoji;
  1423.  
  1424. const labelText = document.createElement('span');
  1425. labelText.textContent = USER_CONFIG.commitTypes[type].label;
  1426. if (isBreaking) {
  1427. labelText.textContent += ' ⚠️';
  1428. }
  1429.  
  1430. label.appendChild(emoji);
  1431. label.appendChild(labelText);
  1432. message.parentElement.insertBefore(label, message);
  1433.  
  1434. // Update the commit message text to remove the type prefix if enabled
  1435. if (USER_CONFIG.removePrefix) {
  1436. message.textContent = restOfMessage;
  1437. }
  1438.  
  1439. // Optionally display scope in the label
  1440. if (scope && USER_CONFIG.showScope) {
  1441. const scopeSpan = document.createElement('span');
  1442. scopeSpan.className = 'commit-scope';
  1443. scopeSpan.textContent = `(${scope})`;
  1444. scopeSpan.style.marginLeft = '2px';
  1445. scopeSpan.style.opacity = '0.8';
  1446. scopeSpan.style.fontSize = '12px';
  1447. labelText.appendChild(scopeSpan);
  1448. }
  1449. } else {
  1450. debugLog('Label already exists, skipping');
  1451. }
  1452. } else {
  1453. debugLog(`No matching commit type found for: ${type}`);
  1454. }
  1455. } else {
  1456. // Only log non-conventional commits if they don't have a label
  1457. if (!message.parentElement.querySelector('.commit-label')) {
  1458. debugLog('Commit message does not match conventional commit format and has no label');
  1459. } else {
  1460. debugLog('Skipping already processed commit');
  1461. }
  1462. }
  1463. } catch (error) {
  1464. debugLog('Error processing commit:', error);
  1465. }
  1466. }
  1467. // Process next batch if needed
  1468. processedCount += (endIndex - startIndex);
  1469. debugLog(`Processed ${processedCount} of ${commitMessagesArray.length} commits`);
  1470. if (processedCount < commitMessagesArray.length) {
  1471. debugLog('Scheduling next batch');
  1472. setTimeout(() => processCommitBatch(endIndex), 0);
  1473. } else {
  1474. debugLog('Finished processing all commits');
  1475. }
  1476. };
  1477. // Start processing first batch
  1478. if (commitMessagesArray.length > 0) {
  1479. debugLog('Starting first batch processing');
  1480. processCommitBatch(0);
  1481. } else {
  1482. debugLog('No commit messages found to process');
  1483. }
  1484. }
  1485.  
  1486. // Set up MutationObserver to watch for DOM changes
  1487. function setupMutationObserver() {
  1488. debugLog('Setting up MutationObserver');
  1489. const observer = new MutationObserver(debounce((mutations) => {
  1490. debugLog('DOM changes detected:', mutations);
  1491. for (const mutation of mutations) {
  1492. if (mutation.addedNodes.length) {
  1493. debugLog('New nodes added, triggering addCommitLabels');
  1494. addCommitLabels();
  1495. }
  1496. }
  1497. }, 100));
  1498.  
  1499. // Start observing the document with the configured parameters
  1500. observer.observe(document.body, {
  1501. childList: true,
  1502. subtree: true
  1503. });
  1504.  
  1505. debugLog('MutationObserver setup complete');
  1506. return observer;
  1507. }
  1508.  
  1509. // Initialize the script
  1510. function initialize() {
  1511. debugLog('Initializing GitHub Commit Labels');
  1512. // Initial run
  1513. addCommitLabels();
  1514.  
  1515. // Set up mutation observer
  1516. const observer = setupMutationObserver();
  1517.  
  1518. // Clean up on page unload
  1519. window.addEventListener('unload', () => {
  1520. debugLog('Cleaning up on page unload');
  1521. observer.disconnect();
  1522. });
  1523. }
  1524.  
  1525. // Initialize on page load
  1526. initialize();
  1527.  
  1528. // Handle GitHub's client-side navigation
  1529. const navigationObserver = new MutationObserver(debounce((mutations) => {
  1530. for (const mutation of mutations) {
  1531. if (mutation.type === 'childList') {
  1532. // Check if we're on a commit page after navigation
  1533. if (isCommitPage()) {
  1534. // Small delay to ensure GitHub has finished rendering
  1535. setTimeout(addCommitLabels, 100);
  1536. }
  1537. }
  1538. }
  1539. }, 100));
  1540.  
  1541. // Observe changes to the main content area
  1542. navigationObserver.observe(document.body, {
  1543. childList: true,
  1544. subtree: true
  1545. });
  1546.  
  1547. // Listen for popstate events (browser back/forward navigation)
  1548. window.addEventListener('popstate', debounce(() => {
  1549. if (isCommitPage()) {
  1550. setTimeout(addCommitLabels, 100);
  1551. }
  1552. }, 100));
  1553.  
  1554. // Listen for GitHub's custom navigation event
  1555. document.addEventListener('turbo:render', debounce(() => {
  1556. if (isCommitPage()) {
  1557. setTimeout(addCommitLabels, 100);
  1558. }
  1559. }, 100));
  1560. })();

QingJ © 2025

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