您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Monitor builds using tab icons
// ==UserScript== // @name Bitbucket Server Pull Request Favicon // @namespace https://github.com/kellyselden // @version 9 // @description Monitor builds using tab icons // @author Kelly Selden // @license MIT // @source https://github.com/kellyselden/bitbucket-server-pull-request-favicon // @supportURL https://github.com/kellyselden/bitbucket-server-pull-request-favicon/issues/new // @include http*://*bitbucket*/projects/*/repos/*/pull-requests/*/* // ==/UserScript== 'use strict'; const icons = { 'build-in-progress-icon': '🔵', 'build-successful-icon': '🟢', 'build-failed-icon': '🔴', }; const statusIconClass = '[data-testid="pull-request-builds-summary"] .build-status-icon'; const vetoesCountClass = '.veto-count'; function getFavicon() { return document.head.querySelector('link[rel="shortcut icon"]'); } function updateFavicon() { let favicon = getFavicon(); let iconText; if (statusIcon) { for (let [_class, icon] of Object.entries(icons)) { if (statusIcon.classList.contains(_class)) { iconText = icon; break; } } if (!iconText) { iconText = '❓'; } } else { iconText = '⚪️'; } if (favicon) { document.head.removeChild(favicon); favicon = null; } if (!favicon) { favicon = document.createElement('link'); favicon.rel = 'shortcut icon'; document.head.appendChild(favicon); } let svg = document.createElement('svg'); svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); let icon = document.createElement('text'); icon.setAttribute('font-size', '13'); icon.setAttribute('y', '13'); icon.textContent = iconText; let number = document.createElement('text'); number.setAttribute('x', '4.5'); number.setAttribute('y', '13'); number.setAttribute('font-size', '13'); number.setAttribute('font-family', 'Helvetica'); number.setAttribute('font-weight', 'bold'); number.textContent = vetoesCount.textContent; svg.appendChild(icon); svg.appendChild(number); favicon.href = `data:image/svg+xml,${svg.outerHTML}`; } let tabContainer = document.querySelector('.pull-request-tabs [role="tabpanel"]'); let statusIcon = tabContainer.querySelector(statusIconClass); let vetoesCountContainer = document.querySelector('.merge-button-container'); let vetoesCount = vetoesCountContainer.querySelector(vetoesCountClass); updateFavicon(); function find(node, query) { if (node.matches?.(query)) { return node; } else { return node.querySelector?.(query); } } let statusIconClassObserver; new MutationObserver(mutationsList => { for (let mutation of mutationsList) { if (mutation.type === 'childList') { for (let node of mutation.removedNodes) { let _statusIcon = find(node, statusIconClass); if (_statusIcon) { statusIcon = null; statusIconClassObserver.disconnect(); statusIconClassObserver = null; updateFavicon(); } } } } if (statusIconClassObserver) { return; } for (let mutation of mutationsList) { if (mutation.type === 'childList') { for (let node of mutation.addedNodes) { let _statusIcon = find(node, statusIconClass); if (_statusIcon) { statusIcon = _statusIcon; updateFavicon(); statusIconClassObserver = new MutationObserver(mutationsList => { for (let mutation of mutationsList) { if (mutation.type === 'childList') { let _statusIcon = find(node, statusIconClass); if (_statusIcon) { statusIcon = _statusIcon; updateFavicon(); } } } }); statusIconClassObserver.observe(statusIcon.closest('.summary-panel'), { subtree: true, childList: true, }); } } } } }).observe(tabContainer, { childList: true, subtree: true, }); new MutationObserver(mutationsList => { for (let mutation of mutationsList) { if (mutation.type === 'childList') { for (let node of mutation.addedNodes) { let _vetoesCount = find(node, vetoesCountClass); if (_vetoesCount) { vetoesCount = _vetoesCount; updateFavicon(); } } } } }).observe(vetoesCountContainer, { childList: true, subtree: true, });