GitHub Commit Labels

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

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

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