您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Jira enhancements.
当前为
// ==UserScript== // @name Devabit Jira+ // @namespace http://tampermonkey.net/ // @version 2.2 // @description Jira enhancements. // @match https://devabit.atlassian.net/browse/* // @match https://devabit.atlassian.net/jira/* // @grant none // @license 3-clause BSD // ==/UserScript== (function() { 'use strict'; const uaMonths = { 'січ': 0, 'лют': 1, 'бер': 2, 'квіт': 3, 'трав': 4, 'черв': 5, 'лип': 6, 'серп': 7, 'вер': 8, 'жовт': 9, 'лист': 10, 'груд': 11 }; const jiraColors = { red: '#a10a0a', green: '#315e1d', yellow: '#ffd414', white: '#ffffff', black: '#000000' }; function parseDueDateString(str) { // Parses date like "16 лип. 2025 р." or "16 лип. 2025 р., 17:00" // Ignores time after comma const regex = /(\d{1,2})\s([а-яіїєґ]{3})\.?\s(\d{4})/i; const m = regex.exec(str); if (!m) return null; return { day: +m[1], month: m[2].toLowerCase(), year: +m[3] }; } function datesMatch(date1, date2) { return date1 && date2 && date1.day === date2.day && date1.month === date2.month && date1.year === date2.year; } function highlightIfDateMismatch() { const dueDateContainer = document.querySelector('div[data-testid="coloured-due-date.ui.colored-due-date-container"]'); const dueDateSpan = document.querySelector('div[data-testid="coloured-due-date.ui.colored-due-date-container"] > span'); if (!dueDateSpan) return; const officialDate = parseDueDateString(dueDateSpan.textContent.trim()); if (!officialDate) return; deadlineMap.forEach((info, el) => { const originalDate = parseDueDateString(info.original); if (!datesMatch(originalDate, officialDate)) { dueDateContainer.style.backgroundColor = jiraColors.red; // red highlight } else { dueDateContainer.style.backgroundColor = ''; // clear highlight if matches } }); } function jiraTimeToHours(input) { const timeUnits = { w: 40, d: 8, h: 1, m: 1 / 60 }; const regex = /(\d+)\s*(w|d|h|m)/gi; let totalHours = 0, match; while ((match = regex.exec(input)) !== null) { totalHours += parseInt(match[1], 10) * timeUnits[match[2].toLowerCase()]; } return +totalHours.toFixed(2); } const estimatedHoursMap = new Map(); function isDelivered() { const el = document.querySelector('button[id="issue.fields.status-view.status-button"] > span.css-178ag6o'); return el && (el.innerText === "Delivered"); } function highlightTagsByDate() { const tags = document.querySelectorAll('span[data-testid="issue.views.common.tag.tag-item"] > span'); const now = new Date(); const currentMonth = now.toLocaleString('en-US', { month: 'long' }); const currentYear = now.getFullYear(); tags.forEach(tag => { // Check for Delivered in sibling span.css-178ag6o const parent = tag.closest('span[data-testid="issue.views.common.tag.tag-item"]'); if (!parent) return; const deliveredSpan = parent.querySelector('span.css-178ag6o'); if (deliveredSpan && deliveredSpan.textContent.includes("Delivered")) { // Skip highlighting & timer for this task parent.style.backgroundColor = ''; parent.style.color = ''; parent.style.border = ''; return; } const text = tag.textContent.trim(); const regex = /^([A-Za-z]+)\s+(\d{4})$/; const match = text.match(regex); if (!match) return; const [_, tagMonth, tagYearStr] = match; const tagYear = parseInt(tagYearStr, 10); parent.style.border = 'none'; // remove border // TODO: handle passed months (do not highlight them red) if (tagMonth.toLowerCase() === currentMonth.toLowerCase() && tagYear === currentYear) { parent.style.backgroundColor = jiraColors.green; // green parent.style.color = 'white'; } else { parent.style.backgroundColor = jiraColors.red; // red parent.style.color = 'white'; } }); } function updateTimeDisplays() { const selectors = [ '.css-v44io0', 'span[data-testid="issue.issue-view.common.logged-time.value"]', 'span[data-testid="issue.component.logged-time.remaining-time"] > span' ]; document.querySelectorAll(selectors.join(',')).forEach(el => { let original = el.getAttribute('data-original'); if (!original) { original = el.textContent.trim(); el.setAttribute('data-original', original); } // Skip non-time strings in css-v44io0 if (el.classList.contains('css-v44io0') && !/[wdhm]/i.test(original)) return; const hours = jiraTimeToHours(original); el.textContent = `${hours}h`; // Save estimate if it’s the main estimate field if (el.classList.contains('css-v44io0')) { estimatedHoursMap.set('estimate', hours); } }); } function highlightTimeInSummary() { const heading = document.querySelector('h1[data-testid="issue.views.issue-base.foundation.summary.heading"]'); if (!heading) return; const original = heading.getAttribute('data-original') || heading.textContent.trim(); heading.setAttribute('data-original', original); const patterns = [ /\b(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{1,2},\s+\d{4}(?:,\s+\d{1,2}:\d{2})?(?:\s+GMT[+-]\d+)?/gi, /\b\d{4}-\d{2}-\d{2}(?:[ T]\d{1,2}:\d{2})?/g, /\b\d{1,2}[./-]\d{1,2}[./-]\d{2,4}/g, /\b\d{1,2}:\d{2}\b/g ]; let highlighted = original; patterns.forEach(pattern => { highlighted = highlighted.replace(pattern, match => `${match}`); }); heading.innerText = highlighted; } function parseUADateTime(str) { const regex = /(\d{1,2})\s([а-яіїєґ]{3,5})\.?\s(\d{4})\sр\.,?\s(\d{1,2}):(\d{2})/i; const m = regex.exec(str); if (!m) return null; const day = parseInt(m[1], 10); const month = uaMonths[m[2].toLowerCase()]; const year = parseInt(m[3], 10); const hour = parseInt(m[4], 10); const minute = parseInt(m[5], 10); if (month === undefined) return null; return new Date(year, month, day, hour, minute); } function formatTimeLeft(ms) { const absMs = Math.abs(ms); const totalSeconds = Math.floor(absMs / 1000); const totalMinutes = Math.floor(totalSeconds / 60); const totalHours = Math.floor(totalMinutes / 60); const totalDays = Math.floor(totalHours / 24); const totalWeeks = Math.floor(totalDays / 7); const totalMonths = Math.floor(totalDays / 30); let parts = []; if (totalMonths >= 1) { parts.push(`${totalMonths}mo`); const remainingDays = totalDays % 30; if (remainingDays) parts.push(`${remainingDays}d`); } else if (totalWeeks >= 1) { parts.push(`${totalWeeks}w`); const remainingDays = totalDays % 7; if (remainingDays) parts.push(`${remainingDays}d`); } else { const hours = Math.floor((totalSeconds % 86400) / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; if (totalDays) parts.push(`${totalDays}d`); if (hours) parts.push(`${hours}h`); if (minutes) parts.push(`${minutes}m`); if (seconds || parts.length === 0) parts.push(`${seconds.toString().padStart(2, '0')}s`); } const label = parts.join(' '); if (ms <= 0) { //return `🔥 ГОРИТЬ — просрочено на ${label}`; return `over deadline by ${label}`; } else { return `${label} left`; } } const deadlineMap = new Map(); function setupLiveDeadlineCountdown() { const containers = document.querySelectorAll('div[data-testid="issue-field-inline-edit-read-view-container.ui.container"]'); containers.forEach(el => { if (!deadlineMap.has(el)) { let original = el.getAttribute('data-original'); if (!original) { original = el.textContent.trim(); el.setAttribute('data-original', original); } const deadline = parseUADateTime(original); if (!deadline) return; deadlineMap.set(el, { deadline, original }); } }); } function findFolderPathMatchingProjectCode() { const heading = document.querySelector('h1[data-testid="issue.views.issue-base.foundation.summary.heading"]'); if (!heading) return null; const title = heading.textContent.trim(); const casePatterns = [ /[A-Z]{2}-[A-Z]{3}\d{7}-\d{3}/g, // full code with -xxx /[A-Z]{2}-[A-Z]{3}\d{7}/g, // code without suffix /\d{4}\/\d{5,6}(?:\/#\d+)?/g ]; let fullProjectCode = null; for (const pattern of casePatterns) { const matches = title.match(pattern); if (matches?.length) { fullProjectCode = matches[matches.length - 1]; break; } } if (!fullProjectCode) return null; // Remove invalid Windows path chars const cleanedCode = fullProjectCode.replace(/[<>:"/\\|?*]/g, ''); const baseCode = cleanedCode.replace(/-\d{3}$/, ''); // strip -xxx if present const escapedBase = baseCode.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const pattern = new RegExp(`M:\\\\[^\\s]*${escapedBase}(?:-\\d{3})?[^\\s]*`, 'gi'); const root = document.querySelector('div.ak-renderer-document'); if (!root) return null; const elements = root.querySelectorAll('p, span, div'); for (const el of elements) { const text = el.innerText.trim(); const match = text.match(pattern); if (match?.length) { return match[0]; // Return first matched path } } return null; } function insertCaseIdFromTitle() { const heading = document.querySelector('h1[data-testid="issue.views.issue-base.foundation.summary.heading"]'); if (!heading) return; const title = heading.textContent.trim(); const casePatterns = [ /[A-Z]{2}-[A-Z]{3}\d{7}-\d{3}/g, /[A-Z]{2}-[A-Z]{3}\d{7}/g, // code without suffix /\d{4}\/\d{5,6}(?:\/#\d+)?/g ]; let match = null; for (const pattern of casePatterns) { const matches = title.match(pattern); if (matches && matches.length) { match = matches[matches.length - 1]; break; } } if (!match) return; // Remove previous overlay if exists const existing = document.getElementById('case-id-overlay'); if (existing) existing.remove(); const deadlineEl = [...deadlineMap.keys()][0]; const deadlineText = deadlineEl?.getAttribute('data-original') || ''; const estimateEl = document.querySelector('div[data-testid="issue-field-inline-edit-read-view-container.ui.container"] > span > span'); const estimateText = estimateEl?.textContent.trim() || ''; const container = document.createElement('div'); container.id = 'case-id-overlay'; Object.assign(container.style, { position: 'fixed', bottom: '0', left: '0', zIndex: '99999', backgroundColor: '#000', color: '#fff', padding: '6px 14px 6px 6px', fontSize: '14px', fontFamily: 'Arial, sans-serif', opacity: '0.95', borderTopRightRadius: '4px', userSelect: 'text' }); const table = document.createElement('table'); Object.assign(table.style, { borderCollapse: 'collapse', width: '100%' }); function createRow(buttonText, valueText) { const tr = document.createElement('tr'); const tdBtn = document.createElement('td'); const btn = document.createElement('button'); btn.textContent = buttonText; Object.assign(btn.style, { background: '#444', color: '#fff', border: 'none', borderRadius: '2px', padding: '2px 8px', cursor: 'pointer', fontSize: '13px', userSelect: 'none', whiteSpace: 'nowrap' }); btn.addEventListener('click', () => { navigator.clipboard.writeText(valueText); btn.textContent = 'Done'; setTimeout(() => { btn.textContent = buttonText; }, 1000); }); tdBtn.appendChild(btn); tdBtn.style.verticalAlign = 'middle'; const tdVal = document.createElement('td'); tdVal.textContent = valueText; tdVal.style.fontWeight = 'normal'; tdVal.style.userSelect = 'text'; tdVal.style.verticalAlign = 'middle'; tdVal.style.padding = '0px'; tr.appendChild(tdBtn); tr.appendChild(tdVal); return tr; } table.appendChild(createRow('Copy', match)); if (deadlineText) table.appendChild(createRow('Copy', deadlineText)); if (estimateText) table.appendChild(createRow('Copy', `${estimateText}`)); const folder = findFolderPathMatchingProjectCode(); if (folder) table.appendChild(createRow('Copy', folder)); container.appendChild(table); document.body.appendChild(container); } function updateLiveCountdowns() { if (isDelivered()) return; const now = new Date(); const estimate = estimatedHoursMap.get('estimate') || 0; deadlineMap.forEach((info, el) => { const msLeft = info.deadline - now; const hoursLeft = msLeft / (1000 * 60 * 60); let label = formatTimeLeft(msLeft); if (!isDelivered()) { el.innerText = `${info.original}\n(${label})`; el.style.whiteSpace = 'pre-line'; el.style.flexDirection = "column"; el.style.gap = "0rem"; el.style.alignItems = "flex-start"; } else { el.textContent = `${info.original}` } // Clear previous style // el.style.backgroundColor = ''; // el.style.mixBlendMode = 'exclusion'; /* if (msLeft <= 0) { el.style.backgroundColor = jiraColors.red; el.style.color = '#ffffff'; } else if (hoursLeft < 0.5) { el.style.backgroundColor = jiraColors.red; // red el.style.color = '#ffffff'; } else if (hoursLeft < estimate) { el.style.backgroundColor = jiraColors.yellow; // yellow el.style.color = '#000000'; } else { el.style.backgroundColor = jiraColors.green; // green el.style.color = '#ffffff'; } */ }); } function replaceLogoWithGif() { const aEl = document.querySelector('a[aria-label="Go to your Jira homepage"]'); if (aEl) { aEl.removeAttribute('style'); aEl.style.textDecoration = 'none'; } const logoWrapper = document.querySelector('span[data-testid="atlassian-navigation--product-home--logo--wrapper"]'); if (!logoWrapper) return; // Remove existing SVG const svg = logoWrapper.querySelector('svg'); if (svg) svg.remove(); // Use flex container logoWrapper.style.display = 'flex'; logoWrapper.style.alignItems = 'center'; logoWrapper.style.gap = '8px'; // Add GIF only once if (!logoWrapper.querySelector('img.devabit-gif')) { const img = document.createElement('img'); img.src = 'https://media.tenor.com/vX-qFMkapQQAAAAj/cat-dancing.gif'; img.className = 'devabit-gif'; img.style.height = '32px'; img.style.width = 'auto'; img.style.verticalAlign = 'middle'; logoWrapper.appendChild(img); } // Add/update Жира span let textSpan = logoWrapper.querySelector('span.devabit-text'); if (!textSpan) { textSpan = document.createElement('span'); textSpan.className = 'devabit-text'; textSpan.textContent = 'жира'; textSpan.style.fontWeight = '700'; textSpan.style.fontFamily = '"Atlassian Sans", Arial, sans-serif'; textSpan.style.fontSize = '14px'; textSpan.style.cursor = 'pointer'; textSpan.style.userSelect = 'none'; textSpan.style.paddingRight = '6px'; textSpan.style.setProperty('text-decoration', 'none', 'important'); logoWrapper.appendChild(textSpan); } textSpan.style.color = '#ffffff'; textSpan.style.mixBlendMode = 'difference'; } function addWallpaperOverlay() { const existing = document.getElementById('jira-top-overlay'); if (existing) { const img = existing.querySelector('img'); if (img) img.style.opacity = opacity; return; } const container = document.createElement('div'); container.id = 'jira-top-overlay'; Object.assign(container.style, { position: 'fixed', top: '0', left: '0', width: '100vw', height: '100vh', zIndex: '999999', pointerEvents: 'none', overflow: 'hidden' }); const img = document.createElement('img'); // winxp img.src = 'https://wallpaperswide.com/download/windows_xp_original-wallpaper-1920x1080.jpg'; // winxp anime // img.src = 'https://c4.wallpaperflare.com/wallpaper/748/833/77/lucky-star-windows-xp-anime-izumi-konata-technology-windows-hd-art-wallpaper-preview.jpg'; // steins;gate //img.src= 'https://wallpapercave.com/wp/wp1858920.jpg' // realmonke //img.src = 'https://www.gstatic.com/mail/themes/featured/f3.jpg=w1680-h1116-e365-fVignette=1,0,1.4,0,000000:Soften=1,0,0:-k-no-nd' Object.assign(img.style, { width: '100%', height: '100%', objectFit: 'cover', opacity: '0.05' }); container.appendChild(img); document.body.appendChild(container); } function debounce(func, delay) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => func.apply(this, args), delay); }; } function isBrowsePage() { return /\/browse\/[A-Z]+-\d+/i.test(location.pathname); } function removeOverlayIfNotBrowse() { if (!isBrowsePage()) { document.getElementById('case-id-overlay')?.remove(); // document.getElementById('jira-top-overlay')?.remove(); } } window.addEventListener('popstate', removeOverlayIfNotBrowse); window.addEventListener('hashchange', removeOverlayIfNotBrowse); function runAllEnhancements() { if (!isBrowsePage()) { removeOverlayIfNotBrowse(); // return; } updateTimeDisplays(); // highlightTimeInSummary(); replaceLogoWithGif(); setupLiveDeadlineCountdown(); updateLiveCountdowns(); highlightTagsByDate(); highlightIfDateMismatch(); insertCaseIdFromTitle(); // addWallpaperOverlay(); } const debouncedUpdate = debounce(runAllEnhancements, 300); const observer = new MutationObserver(debouncedUpdate); observer.observe(document.body, { childList: true, subtree: true }); window.addEventListener('load', runAllEnhancements); setInterval(updateLiveCountdowns, 1000); })();