GitHub Commit Labels

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

目前为 2025-04-14 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name GitHub Commit Labels
  3. // @namespace https://github.com/nazdridoy
  4. // @version 1.5.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. const toggleBtn = document.createElement('button');
  280. toggleBtn.id = 'commit-labels-toggle';
  281. toggleBtn.textContent = USER_CONFIG.labelsVisible ? '🏷️' : '🏷️';
  282. toggleBtn.title = USER_CONFIG.labelsVisible ? 'Hide commit labels' : 'Show commit labels';
  283. toggleBtn.style.cssText = `
  284. position: fixed;
  285. bottom: 20px;
  286. right: 20px;
  287. width: 32px;
  288. height: 32px;
  289. border-radius: 6px;
  290. background: rgba(31, 35, 40, 0.6);
  291. color: #adbac7;
  292. border: 1px solid rgba(205, 217, 229, 0.1);
  293. font-size: 14px;
  294. cursor: pointer;
  295. z-index: 9999;
  296. display: flex;
  297. align-items: center;
  298. justify-content: center;
  299. box-shadow: 0 2px 5px rgba(0,0,0,0.2);
  300. opacity: 0.5;
  301. transition: opacity 0.2s, transform 0.2s, background-color 0.2s;
  302. backdrop-filter: blur(4px);
  303. `;
  304. // Add hover effect
  305. toggleBtn.addEventListener('mouseenter', () => {
  306. toggleBtn.style.opacity = '1';
  307. toggleBtn.style.background = currentTheme === 'light' ?
  308. 'rgba(246, 248, 250, 0.8)' : 'rgba(22, 27, 34, 0.8)';
  309. toggleBtn.style.color = currentTheme === 'light' ? '#24292f' : '#e6edf3';
  310. });
  311. toggleBtn.addEventListener('mouseleave', () => {
  312. toggleBtn.style.opacity = '0.5';
  313. toggleBtn.style.background = 'rgba(31, 35, 40, 0.6)';
  314. toggleBtn.style.color = '#adbac7';
  315. });
  316. // Toggle labels on click
  317. toggleBtn.addEventListener('click', () => {
  318. USER_CONFIG.labelsVisible = !USER_CONFIG.labelsVisible;
  319. GM_setValue('commitLabelsConfig', USER_CONFIG);
  320. // Update button
  321. toggleBtn.textContent = USER_CONFIG.labelsVisible ? '🏷️' : '🏷️';
  322. toggleBtn.style.textDecoration = USER_CONFIG.labelsVisible ? 'none' : 'line-through';
  323. toggleBtn.title = USER_CONFIG.labelsVisible ? 'Hide commit labels' : 'Show commit labels';
  324. // Toggle label visibility
  325. document.querySelectorAll('.commit-label').forEach(label => {
  326. label.style.display = USER_CONFIG.labelsVisible ? 'inline-flex' : 'none';
  327. });
  328. });
  329. document.body.appendChild(toggleBtn);
  330. // Set initial state
  331. toggleBtn.style.textDecoration = USER_CONFIG.labelsVisible ? 'none' : 'line-through';
  332. }
  333.  
  334. // Create configuration window
  335. function createConfigWindow() {
  336. // Get current theme colors for the config window
  337. const isDark = isDarkTheme(currentTheme);
  338. const configStyles = {
  339. window: {
  340. background: isDark ? '#0d1117' : '#ffffff',
  341. border: isDark ? '1px solid #30363d' : '1px solid #d0d7de',
  342. color: isDark ? '#c9d1d9' : '#24292f',
  343. boxShadow: isDark ? '0 0 10px rgba(0,0,0,0.5)' : '0 0 10px rgba(0,0,0,0.2)'
  344. },
  345. button: {
  346. primary: {
  347. background: '#238636',
  348. color: '#ffffff',
  349. border: 'none'
  350. },
  351. secondary: {
  352. background: isDark ? '#21262d' : '#f6f8fa',
  353. color: isDark ? '#c9d1d9' : '#24292f',
  354. border: isDark ? '1px solid #30363d' : '1px solid #d0d7de'
  355. },
  356. danger: {
  357. background: isDark ? '#21262d' : '#f6f8fa',
  358. color: '#f85149',
  359. border: isDark ? '1px solid #30363d' : '1px solid #d0d7de'
  360. }
  361. },
  362. input: {
  363. background: isDark ? '#161b22' : '#f6f8fa',
  364. color: isDark ? '#c9d1d9' : '#24292f',
  365. border: isDark ? '1px solid #30363d' : '1px solid #d0d7de'
  366. },
  367. text: {
  368. dim: isDark ? '#8b949e' : '#6e7781',
  369. link: isDark ? '#58a6ff' : '#0969da'
  370. }
  371. };
  372. const configWindow = document.createElement('div');
  373. configWindow.style.cssText = `
  374. position: fixed;
  375. top: 50%;
  376. left: 50%;
  377. transform: translate(-50%, -50%);
  378. background: ${configStyles.window.background};
  379. border: ${configStyles.window.border};
  380. border-radius: 6px;
  381. padding: 20px;
  382. z-index: 9999;
  383. width: 600px;
  384. max-height: 80vh;
  385. overflow-y: auto;
  386. color: ${configStyles.window.color};
  387. box-shadow: ${configStyles.window.boxShadow};
  388. `;
  389.  
  390. // Header with title and repo link
  391. const titleContainer = document.createElement('div');
  392. titleContainer.style.display = 'flex';
  393. titleContainer.style.justifyContent = 'space-between';
  394. titleContainer.style.alignItems = 'center';
  395. titleContainer.style.marginBottom = '20px';
  396.  
  397. const title = document.createElement('h2');
  398. title.textContent = 'Commit Labels Configuration';
  399. title.style.margin = '0';
  400.  
  401. // Repository link container with profile pic
  402. const repoContainer = document.createElement('div');
  403. repoContainer.style.display = 'flex';
  404. repoContainer.style.alignItems = 'center';
  405. repoContainer.style.gap = '8px';
  406. // Owner profile picture
  407. const profilePic = document.createElement('img');
  408. profilePic.src = 'https://raw.githubusercontent.com/nazdridoy/nazhome/main/public/favicons/nazhome.svg';
  409. profilePic.alt = 'Owner';
  410. profilePic.style.cssText = `
  411. width: 36px;
  412. height: 36px;
  413. border-radius: 50%;
  414. background: ${isDark ? '#30363d' : '#eaeef2'};
  415. padding: 3px;
  416. vertical-align: middle;
  417. `;
  418. const repoLink = document.createElement('a');
  419. repoLink.href = 'https://github.com/nazdridoy/github-commit-labels';
  420. repoLink.target = '_blank';
  421. repoLink.textContent = 'GitHub Repository';
  422. repoLink.style.cssText = `
  423. color: ${configStyles.text.link};
  424. text-decoration: none;
  425. font-size: 15px;
  426. vertical-align: middle;
  427. `;
  428. repoLink.addEventListener('mouseenter', () => {
  429. repoLink.style.textDecoration = 'underline';
  430. });
  431. repoLink.addEventListener('mouseleave', () => {
  432. repoLink.style.textDecoration = 'none';
  433. });
  434.  
  435. repoContainer.appendChild(profilePic);
  436. repoContainer.appendChild(repoLink);
  437. titleContainer.appendChild(title);
  438. titleContainer.appendChild(repoContainer);
  439. configWindow.appendChild(titleContainer);
  440.  
  441. // Remove Prefix Option
  442. const prefixDiv = document.createElement('div');
  443. prefixDiv.style.marginBottom = '20px';
  444. const prefixCheckbox = document.createElement('input');
  445. prefixCheckbox.type = 'checkbox';
  446. prefixCheckbox.checked = USER_CONFIG.removePrefix;
  447. prefixCheckbox.id = 'remove-prefix';
  448. prefixCheckbox.style.marginRight = '5px';
  449. const prefixLabel = document.createElement('label');
  450. prefixLabel.htmlFor = 'remove-prefix';
  451. prefixLabel.textContent = 'Remove commit type prefix from message';
  452. prefixDiv.appendChild(prefixCheckbox);
  453. prefixDiv.appendChild(prefixLabel);
  454. configWindow.appendChild(prefixDiv);
  455.  
  456. // Add toggle for tooltips with preview
  457. const tooltipDiv = document.createElement('div');
  458. tooltipDiv.style.marginBottom = '20px';
  459. const tooltipHeader = document.createElement('div');
  460. tooltipHeader.style.display = 'flex';
  461. tooltipHeader.style.alignItems = 'center';
  462. tooltipHeader.style.marginBottom = '5px';
  463. const tooltipCheckbox = document.createElement('input');
  464. tooltipCheckbox.type = 'checkbox';
  465. tooltipCheckbox.checked = USER_CONFIG.enableTooltips;
  466. tooltipCheckbox.id = 'enable-tooltips';
  467. tooltipCheckbox.style.marginRight = '5px';
  468. const tooltipLabel = document.createElement('label');
  469. tooltipLabel.htmlFor = 'enable-tooltips';
  470. tooltipLabel.textContent = 'Enable tooltips with extended descriptions';
  471. tooltipLabel.style.marginRight = '15px';
  472. // Add tooltip preview
  473. const previewLabel = document.createElement('span');
  474. previewLabel.textContent = 'Preview: ';
  475. previewLabel.style.marginRight = '5px';
  476. const previewExample = document.createElement('span');
  477. previewExample.className = 'tooltip-preview-label';
  478. previewExample.innerHTML = '✨ <span>Feature</span>';
  479. previewExample.dataset.description = 'New user features (not for new files without user features)';
  480. previewExample.style.cssText = `
  481. display: inline-flex;
  482. align-items: center;
  483. justify-content: center;
  484. height: 24px;
  485. padding: 0 10px;
  486. border-radius: 20px;
  487. background: ${isDark ? 'rgba(35, 134, 54, 0.2)' : 'rgba(31, 136, 61, 0.1)'};
  488. color: ${isDark ? '#7ee787' : '#1a7f37'};
  489. cursor: help;
  490. `;
  491. tooltipHeader.appendChild(tooltipCheckbox);
  492. tooltipHeader.appendChild(tooltipLabel);
  493. tooltipHeader.appendChild(previewLabel);
  494. tooltipHeader.appendChild(previewExample);
  495. // Create custom preview tooltip
  496. previewExample.addEventListener('mouseenter', (e) => {
  497. if (!tooltipCheckbox.checked) return;
  498. const tooltipPreview = document.createElement('div');
  499. tooltipPreview.className = 'tooltip-preview';
  500. tooltipPreview.textContent = previewExample.dataset.description;
  501. const rect = e.target.getBoundingClientRect();
  502. tooltipPreview.style.cssText = `
  503. position: fixed;
  504. top: ${rect.bottom + 5}px;
  505. left: ${rect.left}px;
  506. max-width: 300px;
  507. padding: 8px 12px;
  508. color: ${isDark ? '#e6edf3' : '#ffffff'};
  509. text-align: center;
  510. background-color: ${isDark ? '#161b22' : '#24292f'};
  511. border-radius: 6px;
  512. border: ${isDark ? '1px solid #30363d' : '1px solid #d0d7de'};
  513. box-shadow: 0 3px 12px rgba(0,0,0,0.4);
  514. font-size: 12px;
  515. z-index: 10000;
  516. pointer-events: none;
  517. `;
  518. document.body.appendChild(tooltipPreview);
  519. });
  520. previewExample.addEventListener('mouseleave', () => {
  521. const tooltipPreview = document.querySelector('.tooltip-preview');
  522. if (tooltipPreview) {
  523. document.body.removeChild(tooltipPreview);
  524. }
  525. });
  526. tooltipDiv.appendChild(tooltipHeader);
  527. // Add explanation text
  528. const tooltipExplanation = document.createElement('div');
  529. tooltipExplanation.textContent = 'Tooltips show detailed descriptions when hovering over commit labels.';
  530. tooltipExplanation.style.color = configStyles.text.dim;
  531. tooltipExplanation.style.fontSize = '12px';
  532. tooltipExplanation.style.marginTop = '5px';
  533. tooltipDiv.appendChild(tooltipExplanation);
  534. configWindow.insertBefore(tooltipDiv, prefixDiv.nextSibling);
  535.  
  536. // After prefixDiv and tooltipDiv, add a toggle for showing the floating button
  537. const floatingBtnDiv = document.createElement('div');
  538. floatingBtnDiv.style.marginBottom = '20px';
  539. // Add showFloatingButton to USER_CONFIG if it doesn't exist
  540. if (USER_CONFIG.showFloatingButton === undefined) {
  541. USER_CONFIG.showFloatingButton = true;
  542. GM_setValue('commitLabelsConfig', USER_CONFIG);
  543. }
  544. const floatingBtnCheckbox = document.createElement('input');
  545. floatingBtnCheckbox.type = 'checkbox';
  546. floatingBtnCheckbox.checked = USER_CONFIG.showFloatingButton;
  547. floatingBtnCheckbox.id = 'show-floating-btn';
  548. floatingBtnCheckbox.style.marginRight = '5px';
  549. const floatingBtnLabel = document.createElement('label');
  550. floatingBtnLabel.htmlFor = 'show-floating-btn';
  551. floatingBtnLabel.textContent = 'Show floating toggle button';
  552. floatingBtnDiv.appendChild(floatingBtnCheckbox);
  553. floatingBtnDiv.appendChild(floatingBtnLabel);
  554. configWindow.insertBefore(floatingBtnDiv, tooltipDiv.nextSibling);
  555.  
  556. // After the tooltipDiv and before the floatingBtnDiv in the createConfigWindow function:
  557. const scopeDiv = document.createElement('div');
  558. scopeDiv.style.marginBottom = '20px';
  559. const scopeCheckbox = document.createElement('input');
  560. scopeCheckbox.type = 'checkbox';
  561. scopeCheckbox.checked = USER_CONFIG.showScope;
  562. scopeCheckbox.id = 'show-scope';
  563. scopeCheckbox.style.marginRight = '5px';
  564. const scopeLabel = document.createElement('label');
  565. scopeLabel.htmlFor = 'show-scope';
  566. scopeLabel.textContent = 'Show commit scope in labels (e.g., "feat(api): message" shows "api" in label)';
  567. scopeDiv.appendChild(scopeCheckbox);
  568. scopeDiv.appendChild(scopeLabel);
  569. configWindow.insertBefore(scopeDiv, floatingBtnDiv.nextSibling);
  570.  
  571. // Add debug mode toggle
  572. const debugDiv = document.createElement('div');
  573. debugDiv.style.marginBottom = '20px';
  574. const debugCheckbox = document.createElement('input');
  575. debugCheckbox.type = 'checkbox';
  576. debugCheckbox.checked = USER_CONFIG.debugMode;
  577. debugCheckbox.id = 'debug-mode';
  578. debugCheckbox.style.marginRight = '5px';
  579. const debugLabel = document.createElement('label');
  580. debugLabel.htmlFor = 'debug-mode';
  581. debugLabel.textContent = 'Enable debug mode (shows detailed logs in console)';
  582. debugDiv.appendChild(debugCheckbox);
  583. debugDiv.appendChild(debugLabel);
  584. configWindow.insertBefore(debugDiv, prefixDiv.nextSibling);
  585.  
  586. // Commit Types Configuration
  587. const typesContainer = document.createElement('div');
  588. typesContainer.style.marginBottom = '20px';
  589.  
  590. // Group commit types by their label
  591. const groupedTypes = {};
  592. Object.entries(USER_CONFIG.commitTypes).forEach(([type, config]) => {
  593. const key = config.label;
  594. if (!groupedTypes[key]) {
  595. groupedTypes[key] = {
  596. types: [],
  597. config: config
  598. };
  599. }
  600. groupedTypes[key].types.push(type);
  601. });
  602.  
  603. // Create rows for grouped types
  604. Object.entries(groupedTypes).forEach(([label, { types, config }]) => {
  605. const typeDiv = document.createElement('div');
  606. typeDiv.style.marginBottom = '10px';
  607. typeDiv.style.display = 'flex';
  608. typeDiv.style.alignItems = 'center';
  609. typeDiv.style.gap = '10px';
  610.  
  611. // Type names (with aliases) and edit button container
  612. const typeContainer = document.createElement('div');
  613. typeContainer.style.display = 'flex';
  614. typeContainer.style.width = '150px';
  615. typeContainer.style.alignItems = 'center';
  616. typeContainer.style.gap = '4px';
  617.  
  618. const typeSpan = document.createElement('span');
  619. typeSpan.style.color = configStyles.text.dim;
  620. typeSpan.style.flex = '1';
  621. typeSpan.textContent = types.join(', ') + ':';
  622.  
  623. const editAliasButton = document.createElement('button');
  624. editAliasButton.textContent = '✏️';
  625. editAliasButton.title = 'Edit Aliases';
  626. editAliasButton.style.cssText = `
  627. padding: 2px 4px;
  628. background: ${configStyles.button.secondary.background};
  629. color: ${isDark ? '#58a6ff' : '#0969da'};
  630. border: ${configStyles.button.secondary.border};
  631. border-radius: 4px;
  632. cursor: pointer;
  633. font-size: 10px;
  634. `;
  635.  
  636. editAliasButton.onclick = () => {
  637. const currentAliases = types.join(', ');
  638. const newAliases = prompt('Edit aliases (separate with commas):', currentAliases);
  639.  
  640. if (newAliases && newAliases.trim()) {
  641. const newTypes = newAliases.split(',').map(t => t.trim().toLowerCase()).filter(t => t);
  642.  
  643. // Check if any new aliases conflict with other types
  644. const conflictingType = newTypes.find(type =>
  645. USER_CONFIG.commitTypes[type] && !types.includes(type)
  646. );
  647.  
  648. if (conflictingType) {
  649. alert(`The alias "${conflictingType}" already exists in another group!`);
  650. return;
  651. }
  652.  
  653. // Remove old types
  654. types.forEach(type => delete USER_CONFIG.commitTypes[type]);
  655.  
  656. // Add new types with same config
  657. newTypes.forEach(type => {
  658. USER_CONFIG.commitTypes[type] = { ...config };
  659. });
  660.  
  661. // Update the display
  662. typeSpan.textContent = newTypes.join(', ') + ':';
  663.  
  664. // Update dataset for inputs
  665. const inputs = typeDiv.querySelectorAll('input, select');
  666. inputs.forEach(input => {
  667. input.dataset.types = newTypes.join(',');
  668. });
  669. }
  670. };
  671.  
  672. typeContainer.appendChild(typeSpan);
  673. typeContainer.appendChild(editAliasButton);
  674. typeDiv.appendChild(typeContainer);
  675.  
  676. // Emoji input
  677. const emojiInput = document.createElement('input');
  678. emojiInput.type = 'text';
  679. emojiInput.value = config.emoji;
  680. emojiInput.style.width = '40px';
  681. emojiInput.style.background = configStyles.input.background;
  682. emojiInput.style.color = configStyles.input.color;
  683. emojiInput.style.border = configStyles.input.border;
  684. emojiInput.style.borderRadius = '4px';
  685. emojiInput.style.padding = '4px';
  686. emojiInput.dataset.types = types.join(',');
  687. emojiInput.dataset.field = 'emoji';
  688. typeDiv.appendChild(emojiInput);
  689.  
  690. // Label input
  691. const labelInput = document.createElement('input');
  692. labelInput.type = 'text';
  693. labelInput.value = config.label;
  694. labelInput.style.width = '120px';
  695. labelInput.style.background = configStyles.input.background;
  696. labelInput.style.color = configStyles.input.color;
  697. labelInput.style.border = configStyles.input.border;
  698. labelInput.style.borderRadius = '4px';
  699. labelInput.style.padding = '4px';
  700. labelInput.dataset.types = types.join(',');
  701. labelInput.dataset.field = 'label';
  702. typeDiv.appendChild(labelInput);
  703.  
  704. // Color select
  705. const colorSelect = document.createElement('select');
  706. Object.keys(COLORS).forEach(color => {
  707. const option = document.createElement('option');
  708. option.value = color;
  709. option.textContent = color;
  710. if (config.color === color) option.selected = true;
  711. colorSelect.appendChild(option);
  712. });
  713. colorSelect.style.background = configStyles.input.background;
  714. colorSelect.style.color = configStyles.input.color;
  715. colorSelect.style.border = configStyles.input.border;
  716. colorSelect.style.borderRadius = '4px';
  717. colorSelect.style.padding = '4px';
  718. colorSelect.dataset.types = types.join(',');
  719. colorSelect.dataset.field = 'color';
  720. typeDiv.appendChild(colorSelect);
  721.  
  722. // Delete button
  723. const deleteButton = document.createElement('button');
  724. deleteButton.textContent = '🗑️';
  725. deleteButton.style.cssText = `
  726. padding: 2px 8px;
  727. background: ${configStyles.button.danger.background};
  728. color: ${configStyles.button.danger.color};
  729. border: ${configStyles.button.danger.border};
  730. border-radius: 4px;
  731. cursor: pointer;
  732. `;
  733. deleteButton.onclick = () => {
  734. if (confirm(`Delete commit types "${types.join(', ')}"?`)) {
  735. typeDiv.remove();
  736. types.forEach(type => delete USER_CONFIG.commitTypes[type]);
  737. }
  738. };
  739. typeDiv.appendChild(deleteButton);
  740.  
  741. typesContainer.appendChild(typeDiv);
  742. });
  743.  
  744. // Add "Add New Type" button
  745. const addNewButton = document.createElement('button');
  746. addNewButton.textContent = '+ Add New Type';
  747. addNewButton.style.cssText = `
  748. margin-bottom: 15px;
  749. padding: 5px 16px;
  750. background: ${configStyles.button.primary.background};
  751. color: ${configStyles.button.primary.color};
  752. border: ${configStyles.button.primary.border};
  753. border-radius: 6px;
  754. cursor: pointer;
  755. `;
  756.  
  757. addNewButton.onclick = () => {
  758. const typeInput = prompt('Enter the commit type and aliases (separated by commas, e.g., "added, add"):', '');
  759. if (typeInput && typeInput.trim()) {
  760. const types = typeInput.split(',').map(t => t.trim().toLowerCase()).filter(t => t);
  761.  
  762. // Check if any of the types already exist
  763. const existingType = types.find(type => USER_CONFIG.commitTypes[type]);
  764. if (existingType) {
  765. alert(`The commit type "${existingType}" already exists!`);
  766. return;
  767. }
  768.  
  769. // Create base config for all aliases
  770. const baseConfig = {
  771. emoji: '🔄',
  772. label: types[0].charAt(0).toUpperCase() + types[0].slice(1),
  773. color: 'blue',
  774. description: 'Custom commit type'
  775. };
  776.  
  777. // Add all types to config with the same settings
  778. types.forEach(type => {
  779. USER_CONFIG.commitTypes[type] = { ...baseConfig };
  780. });
  781.  
  782. // Create and add new type row
  783. const typeDiv = document.createElement('div');
  784. typeDiv.style.marginBottom = '10px';
  785. typeDiv.style.display = 'flex';
  786. typeDiv.style.alignItems = 'center';
  787. typeDiv.style.gap = '10px';
  788.  
  789. // Type names (with aliases)
  790. const typeSpan = document.createElement('span');
  791. typeSpan.style.width = '150px';
  792. typeSpan.style.color = configStyles.text.dim;
  793. typeSpan.textContent = types.join(', ') + ':';
  794. typeDiv.appendChild(typeSpan);
  795.  
  796. // Emoji input
  797. const emojiInput = document.createElement('input');
  798. emojiInput.type = 'text';
  799. emojiInput.value = baseConfig.emoji;
  800. emojiInput.style.width = '40px';
  801. emojiInput.style.background = configStyles.input.background;
  802. emojiInput.style.color = configStyles.input.color;
  803. emojiInput.style.border = configStyles.input.border;
  804. emojiInput.style.borderRadius = '4px';
  805. emojiInput.style.padding = '4px';
  806. emojiInput.dataset.types = types.join(',');
  807. emojiInput.dataset.field = 'emoji';
  808. typeDiv.appendChild(emojiInput);
  809.  
  810. // Label input
  811. const labelInput = document.createElement('input');
  812. labelInput.type = 'text';
  813. labelInput.value = baseConfig.label;
  814. labelInput.style.width = '120px';
  815. labelInput.style.background = configStyles.input.background;
  816. labelInput.style.color = configStyles.input.color;
  817. labelInput.style.border = configStyles.input.border;
  818. labelInput.style.borderRadius = '4px';
  819. labelInput.style.padding = '4px';
  820. labelInput.dataset.types = types.join(',');
  821. labelInput.dataset.field = 'label';
  822. typeDiv.appendChild(labelInput);
  823.  
  824. // Color select
  825. const colorSelect = document.createElement('select');
  826. Object.keys(COLORS).forEach(color => {
  827. const option = document.createElement('option');
  828. option.value = color;
  829. option.textContent = color;
  830. if (color === 'blue') option.selected = true;
  831. colorSelect.appendChild(option);
  832. });
  833. colorSelect.style.background = configStyles.input.background;
  834. colorSelect.style.color = configStyles.input.color;
  835. colorSelect.style.border = configStyles.input.border;
  836. colorSelect.style.borderRadius = '4px';
  837. colorSelect.style.padding = '4px';
  838. colorSelect.dataset.types = types.join(',');
  839. colorSelect.dataset.field = 'color';
  840. typeDiv.appendChild(colorSelect);
  841.  
  842. // Delete button
  843. const deleteButton = document.createElement('button');
  844. deleteButton.textContent = '🗑️';
  845. deleteButton.style.cssText = `
  846. padding: 2px 8px;
  847. background: ${configStyles.button.danger.background};
  848. color: ${configStyles.button.danger.color};
  849. border: ${configStyles.button.danger.border};
  850. border-radius: 4px;
  851. cursor: pointer;
  852. `;
  853. deleteButton.onclick = () => {
  854. if (confirm(`Delete commit types "${types.join(', ')}"?`)) {
  855. typeDiv.remove();
  856. types.forEach(type => delete USER_CONFIG.commitTypes[type]);
  857. }
  858. };
  859. typeDiv.appendChild(deleteButton);
  860.  
  861. typesContainer.appendChild(typeDiv);
  862. }
  863. };
  864.  
  865. configWindow.appendChild(addNewButton);
  866. configWindow.appendChild(typesContainer);
  867.  
  868. // Save and Close buttons
  869. const buttonContainer = document.createElement('div');
  870. buttonContainer.style.display = 'flex';
  871. buttonContainer.style.gap = '10px';
  872. buttonContainer.style.justifyContent = 'flex-end';
  873.  
  874. const saveButton = document.createElement('button');
  875. saveButton.textContent = 'Save';
  876. saveButton.style.cssText = `
  877. padding: 5px 16px;
  878. background: ${configStyles.button.primary.background};
  879. color: ${configStyles.button.primary.color};
  880. border: ${configStyles.button.primary.border};
  881. border-radius: 6px;
  882. cursor: pointer;
  883. `;
  884.  
  885. const closeButton = document.createElement('button');
  886. closeButton.textContent = 'Close';
  887. closeButton.style.cssText = `
  888. padding: 5px 16px;
  889. background: ${configStyles.button.secondary.background};
  890. color: ${configStyles.button.secondary.color};
  891. border: ${configStyles.button.secondary.border};
  892. border-radius: 6px;
  893. cursor: pointer;
  894. `;
  895.  
  896. // Add Reset button next to Save and Close
  897. const resetButton = document.createElement('button');
  898. resetButton.textContent = 'Reset to Default';
  899. resetButton.style.cssText = `
  900. padding: 5px 16px;
  901. background: ${configStyles.button.danger.background};
  902. color: ${configStyles.button.danger.color};
  903. border: ${configStyles.button.danger.border};
  904. border-radius: 6px;
  905. cursor: pointer;
  906. margin-right: auto; // This pushes Save/Close to the right
  907. `;
  908.  
  909. resetButton.onclick = () => {
  910. if (confirm('Are you sure you want to reset all settings to default? This will remove all custom types and settings.')) {
  911. GM_setValue('commitLabelsConfig', DEFAULT_CONFIG);
  912. location.reload();
  913. }
  914. };
  915.  
  916. saveButton.onclick = () => {
  917. const newConfig = { ...USER_CONFIG };
  918. newConfig.removePrefix = prefixCheckbox.checked;
  919. newConfig.enableTooltips = tooltipCheckbox.checked;
  920. newConfig.showFloatingButton = floatingBtnCheckbox.checked;
  921. newConfig.showScope = scopeCheckbox.checked;
  922. newConfig.debugMode = debugCheckbox.checked; // Add debug mode
  923. newConfig.commitTypes = {};
  924.  
  925. typesContainer.querySelectorAll('input, select').forEach(input => {
  926. const types = input.dataset.types.split(',');
  927. const field = input.dataset.field;
  928.  
  929. types.forEach(type => {
  930. if (!newConfig.commitTypes[type]) {
  931. newConfig.commitTypes[type] = {};
  932. }
  933. newConfig.commitTypes[type][field] = input.value;
  934. });
  935. });
  936.  
  937. GM_setValue('commitLabelsConfig', newConfig);
  938. location.reload();
  939. };
  940.  
  941. closeButton.onclick = () => {
  942. document.body.removeChild(configWindow);
  943. };
  944.  
  945. buttonContainer.appendChild(resetButton);
  946. buttonContainer.appendChild(closeButton);
  947. buttonContainer.appendChild(saveButton);
  948. configWindow.appendChild(buttonContainer);
  949.  
  950. document.body.appendChild(configWindow);
  951. }
  952.  
  953. // Create export/import dialog
  954. function createExportImportDialog() {
  955. // Check if dialog already exists
  956. if (document.getElementById('config-export-import')) {
  957. document.getElementById('config-export-import').remove();
  958. }
  959. const dialog = document.createElement('div');
  960. dialog.id = 'config-export-import';
  961. dialog.style.cssText = `
  962. position: fixed;
  963. top: 50%;
  964. left: 50%;
  965. transform: translate(-50%, -50%);
  966. background: #0d1117;
  967. border: 1px solid #30363d;
  968. border-radius: 6px;
  969. padding: 20px;
  970. z-index: 9999;
  971. width: 500px;
  972. max-height: 80vh;
  973. overflow-y: auto;
  974. color: #c9d1d9;
  975. box-shadow: 0 0 20px rgba(0,0,0,0.7);
  976. `;
  977. const title = document.createElement('h2');
  978. title.textContent = 'Export/Import Configuration';
  979. title.style.marginBottom = '15px';
  980. const exportSection = document.createElement('div');
  981. exportSection.style.marginBottom = '20px';
  982. const exportTitle = document.createElement('h3');
  983. exportTitle.textContent = 'Export Configuration';
  984. exportTitle.style.marginBottom = '10px';
  985. const configOutput = document.createElement('textarea');
  986. configOutput.readOnly = true;
  987. configOutput.value = JSON.stringify(USER_CONFIG, null, 2);
  988. configOutput.style.cssText = `
  989. width: 100%;
  990. height: 150px;
  991. background: #161b22;
  992. color: #c9d1d9;
  993. border: 1px solid #30363d;
  994. border-radius: 6px;
  995. padding: 10px;
  996. font-family: monospace;
  997. resize: vertical;
  998. margin-bottom: 10px;
  999. `;
  1000. const copyButton = document.createElement('button');
  1001. copyButton.textContent = 'Copy to Clipboard';
  1002. copyButton.style.cssText = `
  1003. padding: 6px 16px;
  1004. background: #238636;
  1005. color: #fff;
  1006. border: none;
  1007. border-radius: 6px;
  1008. cursor: pointer;
  1009. margin-right: 10px;
  1010. `;
  1011. copyButton.onclick = () => {
  1012. configOutput.select();
  1013. document.execCommand('copy');
  1014. copyButton.textContent = 'Copied!';
  1015. setTimeout(() => {
  1016. copyButton.textContent = 'Copy to Clipboard';
  1017. }, 2000);
  1018. };
  1019. exportSection.appendChild(exportTitle);
  1020. exportSection.appendChild(configOutput);
  1021. exportSection.appendChild(copyButton);
  1022. const importSection = document.createElement('div');
  1023. importSection.style.marginBottom = '20px';
  1024. const importTitle = document.createElement('h3');
  1025. importTitle.textContent = 'Import Configuration';
  1026. importTitle.style.marginBottom = '10px';
  1027. const configInput = document.createElement('textarea');
  1028. configInput.placeholder = 'Paste configuration JSON here...';
  1029. configInput.style.cssText = `
  1030. width: 100%;
  1031. height: 150px;
  1032. background: #161b22;
  1033. color: #c9d1d9;
  1034. border: 1px solid #30363d;
  1035. border-radius: 6px;
  1036. padding: 10px;
  1037. font-family: monospace;
  1038. resize: vertical;
  1039. margin-bottom: 10px;
  1040. `;
  1041. const importButton = document.createElement('button');
  1042. importButton.textContent = 'Import';
  1043. importButton.style.cssText = `
  1044. padding: 6px 16px;
  1045. background: #238636;
  1046. color: #fff;
  1047. border: none;
  1048. border-radius: 6px;
  1049. cursor: pointer;
  1050. margin-right: 10px;
  1051. `;
  1052. importButton.onclick = () => {
  1053. try {
  1054. const newConfig = JSON.parse(configInput.value);
  1055. // Validate basic structure
  1056. if (!newConfig.commitTypes) {
  1057. throw new Error('Invalid configuration: missing commitTypes object');
  1058. }
  1059. if (confirm('Are you sure you want to import this configuration? This will overwrite your current settings.')) {
  1060. GM_setValue('commitLabelsConfig', newConfig);
  1061. alert('Configuration imported successfully! Page will reload to apply changes.');
  1062. location.reload();
  1063. }
  1064. } catch (error) {
  1065. alert('Error importing configuration: ' + error.message);
  1066. }
  1067. };
  1068. const closeButton = document.createElement('button');
  1069. closeButton.textContent = 'Close';
  1070. closeButton.style.cssText = `
  1071. padding: 6px 16px;
  1072. background: #21262d;
  1073. color: #c9d1d9;
  1074. border: 1px solid #30363d;
  1075. border-radius: 6px;
  1076. cursor: pointer;
  1077. `;
  1078. closeButton.onclick = () => {
  1079. document.body.removeChild(dialog);
  1080. };
  1081. importSection.appendChild(importTitle);
  1082. importSection.appendChild(configInput);
  1083. importSection.appendChild(importButton);
  1084. dialog.appendChild(title);
  1085. dialog.appendChild(exportSection);
  1086. dialog.appendChild(importSection);
  1087. dialog.appendChild(closeButton);
  1088. document.body.appendChild(dialog);
  1089. }
  1090.  
  1091. // Register configuration menu command
  1092. GM_registerMenuCommand('Configure Commit Labels', createConfigWindow);
  1093. GM_registerMenuCommand('Toggle Labels', () => {
  1094. USER_CONFIG.labelsVisible = !USER_CONFIG.labelsVisible;
  1095. GM_setValue('commitLabelsConfig', USER_CONFIG);
  1096. // Toggle label visibility
  1097. document.querySelectorAll('.commit-label').forEach(label => {
  1098. label.style.display = USER_CONFIG.labelsVisible ? 'inline-flex' : 'none';
  1099. });
  1100. // Update toggle button if it exists
  1101. const toggleBtn = document.getElementById('commit-labels-toggle');
  1102. if (toggleBtn) {
  1103. toggleBtn.textContent = USER_CONFIG.labelsVisible ? '🏷️' : '🏷️';
  1104. toggleBtn.style.textDecoration = USER_CONFIG.labelsVisible ? 'none' : 'line-through';
  1105. toggleBtn.title = USER_CONFIG.labelsVisible ? 'Hide commit labels' : 'Show commit labels';
  1106. }
  1107. });
  1108. GM_registerMenuCommand('Export/Import Config', createExportImportDialog);
  1109.  
  1110. // Check if we're on a commit page
  1111. function isCommitPage() {
  1112. return window.location.pathname.includes('/commits') ||
  1113. window.location.pathname.includes('/commit/');
  1114. }
  1115.  
  1116. // Update colors when theme changes
  1117. function updateThemeColors() {
  1118. const newTheme = detectTheme();
  1119. if (newTheme !== currentTheme) {
  1120. currentTheme = newTheme;
  1121. // Map theme variants to our base themes for colors
  1122. let baseTheme = newTheme;
  1123. if (newTheme.startsWith('light_')) {
  1124. baseTheme = 'light';
  1125. } else if (newTheme.startsWith('dark_') && newTheme !== 'dark_dimmed') {
  1126. baseTheme = 'dark';
  1127. }
  1128. COLORS = THEME_COLORS[baseTheme] || THEME_COLORS.light;
  1129. // Update existing labels
  1130. document.querySelectorAll('.commit-label').forEach(label => {
  1131. const type = label.dataset.commitType;
  1132. if (type && USER_CONFIG.commitTypes[type]) {
  1133. const color = COLORS[USER_CONFIG.commitTypes[type].color];
  1134. if (color) {
  1135. label.style.backgroundColor = color.bg;
  1136. label.style.color = color.text;
  1137. }
  1138. }
  1139. });
  1140. }
  1141. }
  1142.  
  1143. // Debug logging function
  1144. function debugLog(message, data = null) {
  1145. if (USER_CONFIG.debugMode) {
  1146. const timestamp = new Date().toISOString();
  1147. const logMessage = `[GitHub Commit Labels Debug] [${timestamp}] ${message}`;
  1148. console.log(logMessage);
  1149. if (data) {
  1150. console.log('Data:', data);
  1151. }
  1152. }
  1153. }
  1154.  
  1155. // Helper function to safely query elements
  1156. function safeQuerySelector(selector) {
  1157. try {
  1158. const elements = document.querySelectorAll(selector);
  1159. debugLog(`Query selector "${selector}" found ${elements.length} elements`);
  1160. return elements;
  1161. } catch (error) {
  1162. debugLog(`Selector error for "${selector}":`, error);
  1163. return [];
  1164. }
  1165. }
  1166.  
  1167. // Debounce function to limit how often a function can be called
  1168. function debounce(func, wait) {
  1169. let timeout;
  1170. return function executedFunction(...args) {
  1171. const later = () => {
  1172. clearTimeout(timeout);
  1173. func(...args);
  1174. };
  1175. clearTimeout(timeout);
  1176. timeout = setTimeout(later, wait);
  1177. };
  1178. }
  1179.  
  1180. // Main function to add labels to commits
  1181. function addCommitLabels() {
  1182. debugLog('Starting addCommitLabels');
  1183. // Only proceed if we're on a commit page
  1184. if (!isCommitPage()) {
  1185. debugLog('Not on a commit page, exiting');
  1186. return;
  1187. }
  1188.  
  1189. debugLog('Updating theme colors');
  1190. updateThemeColors();
  1191. // Create toggle button if it doesn't exist and is enabled
  1192. if (USER_CONFIG.showFloatingButton !== false) {
  1193. debugLog('Creating label toggle button');
  1194. createLabelToggle();
  1195. }
  1196.  
  1197. // Try multiple selectors in order of reliability
  1198. const selectors = [
  1199. 'li[data-testid="commit-row-item"] h4 a[data-pjax="true"]', // Most reliable
  1200. '.Title-module__heading--upUxW a[data-pjax="true"]', // Backup
  1201. '.markdown-title a[data-pjax="true"]' // Legacy
  1202. ];
  1203.  
  1204. debugLog('Trying selectors:', selectors);
  1205. let commitMessages = [];
  1206. for (const selector of selectors) {
  1207. commitMessages = safeQuerySelector(selector);
  1208. if (commitMessages.length > 0) {
  1209. debugLog(`Using selector: ${selector}`);
  1210. break;
  1211. }
  1212. }
  1213.  
  1214. debugLog(`Found ${commitMessages.length} commit messages to process`);
  1215.  
  1216. // Debounce and batch process for performance improvement
  1217. let processedCount = 0;
  1218. const batchSize = 20;
  1219. const commitMessagesArray = Array.from(commitMessages);
  1220. const processCommitBatch = (startIndex) => {
  1221. debugLog(`Processing batch starting at index ${startIndex}`);
  1222. const endIndex = Math.min(startIndex + batchSize, commitMessagesArray.length);
  1223. for (let i = startIndex; i < endIndex; i++) {
  1224. try {
  1225. const message = commitMessagesArray[i];
  1226. const text = message.textContent.trim();
  1227. debugLog(`Processing commit message: ${text}`);
  1228. // Skip if this commit already has a label
  1229. if (message.parentElement.querySelector('.commit-label')) {
  1230. debugLog('Commit already has a label, skipping');
  1231. continue;
  1232. }
  1233.  
  1234. const match = text.match(/^(\w+)(?:\(([\w-]+)\))?:\s*(.*)/);
  1235. debugLog('Commit message match result:', match);
  1236.  
  1237. if (match) {
  1238. const type = match[1].toLowerCase();
  1239. const scope = match[2] || '';
  1240. const restOfMessage = match[3];
  1241. debugLog(`Extracted: type=${type}, scope=${scope}, message=${restOfMessage}`);
  1242.  
  1243. if (USER_CONFIG.commitTypes[type]) {
  1244. debugLog(`Found matching commit type: ${type}`);
  1245. // Only add label if it hasn't been added yet
  1246. if (!message.parentElement.querySelector('.commit-label')) {
  1247. debugLog('Creating new label');
  1248. const label = document.createElement('span');
  1249. label.className = 'commit-label';
  1250. label.dataset.commitType = type;
  1251. label.dataset.commitScope = scope;
  1252. const color = COLORS[USER_CONFIG.commitTypes[type].color];
  1253. // Apply styles
  1254. const styles = {
  1255. ...USER_CONFIG.labelStyle,
  1256. backgroundColor: color.bg,
  1257. color: color.text,
  1258. display: USER_CONFIG.labelsVisible ? 'inline-flex' : 'none'
  1259. };
  1260.  
  1261. label.style.cssText = Object.entries(styles)
  1262. .map(([key, value]) => `${key.replace(/[A-Z]/g, m => '-' + m.toLowerCase())}: ${value}`)
  1263. .join(';');
  1264.  
  1265. // Enhanced tooltip
  1266. if (USER_CONFIG.enableTooltips && USER_CONFIG.commitTypes[type].description) {
  1267. // Store description in data attribute instead of title to avoid double tooltips
  1268. const description = USER_CONFIG.commitTypes[type].description;
  1269. const tooltipText = scope ?
  1270. `${description} (Scope: ${scope})` :
  1271. description;
  1272. label.dataset.description = tooltipText;
  1273. label.setAttribute('aria-label', tooltipText);
  1274. // Add tooltip indicator
  1275. label.style.cursor = 'help';
  1276. // For better accessibility
  1277. label.setAttribute('role', 'tooltip');
  1278. // Create a custom tooltip implementation if needed
  1279. label.addEventListener('mouseenter', (e) => {
  1280. // Check if we already have a custom tooltip showing
  1281. if (document.querySelector('.commit-label-tooltip')) {
  1282. return;
  1283. }
  1284. label.style.transform = 'translateY(-1px)';
  1285. label.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)';
  1286. // Force show tooltip by creating a custom one
  1287. if (label.dataset.description) {
  1288. const tooltip = document.createElement('div');
  1289. tooltip.className = 'commit-label-tooltip';
  1290. tooltip.textContent = label.dataset.description;
  1291. // Calculate position relative to viewport
  1292. const rect = e.target.getBoundingClientRect();
  1293. const top = rect.bottom + 5;
  1294. const left = rect.left;
  1295. tooltip.style.cssText = `
  1296. position: fixed;
  1297. top: ${top}px;
  1298. left: ${left}px;
  1299. max-width: 300px;
  1300. padding: 8px 12px;
  1301. color: #e6edf3;
  1302. text-align: center;
  1303. background-color: #161b22;
  1304. border-radius: 6px;
  1305. border: 1px solid #30363d;
  1306. box-shadow: 0 3px 12px rgba(0,0,0,0.4);
  1307. font-size: 12px;
  1308. z-index: 1000;
  1309. pointer-events: none;
  1310. `;
  1311. document.body.appendChild(tooltip);
  1312. // Adjust position if tooltip goes off-screen
  1313. const tooltipRect = tooltip.getBoundingClientRect();
  1314. if (tooltipRect.right > window.innerWidth) {
  1315. tooltip.style.left = `${window.innerWidth - tooltipRect.width - 10}px`;
  1316. }
  1317. }
  1318. });
  1319.  
  1320. label.addEventListener('mouseleave', () => {
  1321. label.style.transform = 'translateY(0)';
  1322. label.style.boxShadow = styles.boxShadow;
  1323. // Remove custom tooltip if it exists
  1324. const tooltip = document.querySelector('.commit-label-tooltip');
  1325. if (tooltip) {
  1326. document.body.removeChild(tooltip);
  1327. }
  1328. });
  1329. } else {
  1330. // Normal hover effect if tooltips are disabled
  1331. if (USER_CONFIG.commitTypes[type].description) {
  1332. label.title = USER_CONFIG.commitTypes[type].description;
  1333. }
  1334. label.addEventListener('mouseenter', () => {
  1335. label.style.transform = 'translateY(-1px)';
  1336. label.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)';
  1337. });
  1338.  
  1339. label.addEventListener('mouseleave', () => {
  1340. label.style.transform = 'translateY(0)';
  1341. label.style.boxShadow = styles.boxShadow;
  1342. });
  1343. }
  1344.  
  1345. const emoji = document.createElement('span');
  1346. emoji.style.marginRight = '4px';
  1347. emoji.style.fontSize = '14px';
  1348. emoji.style.lineHeight = '1';
  1349. emoji.textContent = USER_CONFIG.commitTypes[type].emoji;
  1350.  
  1351. const labelText = document.createElement('span');
  1352. labelText.textContent = USER_CONFIG.commitTypes[type].label;
  1353.  
  1354. label.appendChild(emoji);
  1355. label.appendChild(labelText);
  1356. message.parentElement.insertBefore(label, message);
  1357.  
  1358. // Update the commit message text to remove the type prefix if enabled
  1359. if (USER_CONFIG.removePrefix) {
  1360. message.textContent = restOfMessage;
  1361. }
  1362.  
  1363. // Optionally display scope in the label
  1364. if (scope && USER_CONFIG.showScope) {
  1365. const scopeSpan = document.createElement('span');
  1366. scopeSpan.className = 'commit-scope';
  1367. scopeSpan.textContent = `(${scope})`;
  1368. scopeSpan.style.marginLeft = '2px';
  1369. scopeSpan.style.opacity = '0.8';
  1370. scopeSpan.style.fontSize = '12px';
  1371. labelText.appendChild(scopeSpan);
  1372. }
  1373. } else {
  1374. debugLog('Label already exists, skipping');
  1375. }
  1376. } else {
  1377. debugLog(`No matching commit type found for: ${type}`);
  1378. }
  1379. } else {
  1380. // Only log non-conventional commits if they don't have a label
  1381. if (!message.parentElement.querySelector('.commit-label')) {
  1382. debugLog('Commit message does not match conventional commit format and has no label');
  1383. } else {
  1384. debugLog('Skipping already processed commit');
  1385. }
  1386. }
  1387. } catch (error) {
  1388. debugLog('Error processing commit:', error);
  1389. }
  1390. }
  1391. // Process next batch if needed
  1392. processedCount += (endIndex - startIndex);
  1393. debugLog(`Processed ${processedCount} of ${commitMessagesArray.length} commits`);
  1394. if (processedCount < commitMessagesArray.length) {
  1395. debugLog('Scheduling next batch');
  1396. setTimeout(() => processCommitBatch(endIndex), 0);
  1397. } else {
  1398. debugLog('Finished processing all commits');
  1399. }
  1400. };
  1401. // Start processing first batch
  1402. if (commitMessagesArray.length > 0) {
  1403. debugLog('Starting first batch processing');
  1404. processCommitBatch(0);
  1405. } else {
  1406. debugLog('No commit messages found to process');
  1407. }
  1408. }
  1409.  
  1410. // Set up MutationObserver to watch for DOM changes
  1411. function setupMutationObserver() {
  1412. debugLog('Setting up MutationObserver');
  1413. const observer = new MutationObserver(debounce((mutations) => {
  1414. debugLog('DOM changes detected:', mutations);
  1415. for (const mutation of mutations) {
  1416. if (mutation.addedNodes.length) {
  1417. debugLog('New nodes added, triggering addCommitLabels');
  1418. addCommitLabels();
  1419. }
  1420. }
  1421. }, 100));
  1422.  
  1423. // Start observing the document with the configured parameters
  1424. observer.observe(document.body, {
  1425. childList: true,
  1426. subtree: true
  1427. });
  1428.  
  1429. debugLog('MutationObserver setup complete');
  1430. return observer;
  1431. }
  1432.  
  1433. // Initialize the script
  1434. function initialize() {
  1435. debugLog('Initializing GitHub Commit Labels');
  1436. // Initial run
  1437. addCommitLabels();
  1438.  
  1439. // Set up mutation observer
  1440. const observer = setupMutationObserver();
  1441.  
  1442. // Clean up on page unload
  1443. window.addEventListener('unload', () => {
  1444. debugLog('Cleaning up on page unload');
  1445. observer.disconnect();
  1446. });
  1447. }
  1448.  
  1449. // Initialize on page load
  1450. initialize();
  1451.  
  1452. // Handle GitHub's client-side navigation
  1453. const navigationObserver = new MutationObserver(debounce((mutations) => {
  1454. for (const mutation of mutations) {
  1455. if (mutation.type === 'childList') {
  1456. // Check if we're on a commit page after navigation
  1457. if (isCommitPage()) {
  1458. // Small delay to ensure GitHub has finished rendering
  1459. setTimeout(addCommitLabels, 100);
  1460. }
  1461. }
  1462. }
  1463. }, 100));
  1464.  
  1465. // Observe changes to the main content area
  1466. navigationObserver.observe(document.body, {
  1467. childList: true,
  1468. subtree: true
  1469. });
  1470.  
  1471. // Listen for popstate events (browser back/forward navigation)
  1472. window.addEventListener('popstate', debounce(() => {
  1473. if (isCommitPage()) {
  1474. setTimeout(addCommitLabels, 100);
  1475. }
  1476. }, 100));
  1477.  
  1478. // Listen for GitHub's custom navigation event
  1479. document.addEventListener('turbo:render', debounce(() => {
  1480. if (isCommitPage()) {
  1481. setTimeout(addCommitLabels, 100);
  1482. }
  1483. }, 100));
  1484. })();