您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Changes jira display for custom label styling and caches labels in local storage for swimlane view. Includes a settings panel.
当前为
// ==UserScript== // @name Jira - Custom script specific for work usecase // @namespace http://tampermonkey.net/ // @version 7.5 // @description Changes jira display for custom label styling and caches labels in local storage for swimlane view. Includes a settings panel. // @author Roy // @match https://jira.onderwijstransparant.nl/* // @grant none // ==/UserScript== (function() { 'use strict'; // --- INSTELLINGEN BEHEER --- /** * Laadt de instellingen uit localStorage. Geeft standaardwaarden terug als er geen zijn. * @returns {Object} Het instellingen-object. */ function loadSettings() { const defaults = { highlightBugs: true, setBillableToNo: true, setDefaultComponent: false, defaultComponent: 'OT Software', setDefaultRegion: false, }; try { const savedSettings = localStorage.getItem('jiraCustomScriptSettings'); return savedSettings ? { ...defaults, ...JSON.parse(savedSettings) } : defaults; } catch (e) { console.error('Fout bij het laden van instellingen:', e); return defaults; } } /** * Slaat het instellingen-object op in localStorage. * @param {Object} settings - Het instellingen-object om op te slaan. */ function saveSettings(settings) { try { localStorage.setItem('jiraCustomScriptSettings', JSON.stringify(settings)); } catch (e) { console.error('Fout bij het opslaan van instellingen:', e); } } // --- FUNCTIES VOOR LOKALE OPSLAG (CACHE) --- function getLabelCache() { try { const cachedData = localStorage.getItem('jiraLabelCache'); return cachedData ? JSON.parse(cachedData) : {}; } catch (e) { console.error('Fout bij het lezen van de Jira label-cache:', e); return {}; } } function saveLabelCache(cache) { try { localStorage.setItem('jiraLabelCache', JSON.stringify(cache)); } catch (e) { console.error('Fout bij het opslaan van de Jira label-cache:', e); } } // --- UI INJECTIE (INSTELLINGEN-PANEEL) --- /** * Creëert en injecteert de UI voor het instellingenpaneel en de knop om het te openen. */ function setupSettingsUI() { if (document.getElementById('jira-custom-settings-modal')) return; // Voorkom dubbel injecteren const modal = document.createElement('div'); modal.id = 'jira-custom-settings-modal'; Object.assign(modal.style, { display: 'none', position: 'fixed', zIndex: '1001', left: '50%', top: '50%', transform: 'translate(-50%, -50%)', backgroundColor: '#f5f5f5', padding: '25px', border: '1px solid #ccc', borderRadius: '8px', boxShadow: '0 4px 15px rgba(0,0,0,0.2)', minWidth: '400px' }); const overlay = document.createElement('div'); overlay.id = 'jira-custom-settings-overlay'; Object.assign(overlay.style, { display: 'none', position: 'fixed', zIndex: '1000', left: '0', top: '0', width: '100%', height: '100%', backgroundColor: 'rgba(0,0,0,0.5)' }); overlay.onclick = () => { modal.style.display = 'none'; overlay.style.display = 'none'; }; const currentSettings = loadSettings(); modal.innerHTML = ` <h3 style="margin-top: 0; border-bottom: 1px solid #ddd; padding-bottom: 10px;">Script Instellingen</h3> <div> <label style="display: flex; align-items: center; cursor: pointer; padding: 8px 0;"> <input type="checkbox" id="setting-highlight-bugs" ${currentSettings.highlightBugs ? 'checked' : ''}> <span style="margin-left: 8px;">Bugs een rode achtergrond geven</span> </label> <label style="display: flex; align-items: center; cursor: pointer; padding: 8px 0;"> <input type="checkbox" id="setting-set-billable" ${currentSettings.setBillableToNo ? 'checked' : ''}> <span style="margin-left: 8px;">'Facturabel' standaard op "No" zetten (bij aanmaken)</span> </label> <label style="display: flex; align-items: center; cursor: pointer; padding: 8px 0;"> <input type="checkbox" id="setting-default-region" ${currentSettings.setDefaultRegion ? 'checked' : ''}> <span style="margin-left: 8px;">Standaard regio op "ALG" zetten</span> </label> <label style="display: flex; align-items: center; cursor: pointer; padding: 8px 0;"> <input type="checkbox" id="setting-default-component" ${currentSettings.setDefaultComponent ? 'checked' : ''}> <span style="margin-left: 8px;">Standaard component instellen:</span> </label> <select id="setting-component-choice" style="margin-left: 30px; padding: 4px;" ${!currentSettings.setDefaultComponent ? 'disabled' : ''}> <option value="OT Software" ${currentSettings.defaultComponent === 'OT Software' ? 'selected' : ''}>OT Software</option> <option value="OT Producten" ${currentSettings.defaultComponent === 'OT Producten' ? 'selected' : ''}>OT Producten</option> </select> </div> <div style="margin-top: 20px; text-align: right;"> <button id="save-settings-btn" style="padding: 8px 16px; border: none; background-color: #0052cc; color: white; border-radius: 3px; cursor: pointer;">Opslaan en sluiten</button> </div> `; const settingsButton = document.createElement('div'); settingsButton.innerHTML = '⚙️'; Object.assign(settingsButton.style, { position: 'fixed', bottom: '20px', right: '20px', zIndex: '999', backgroundColor: '#fff', border: '1px solid #ccc', borderRadius: '50%', width: '40px', height: '40px', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', boxShadow: '0 2px 5px rgba(0,0,0,0.2)', fontSize: '24px' }); settingsButton.title = 'Jira Script Instellingen'; settingsButton.onclick = () => { modal.style.display = 'block'; overlay.style.display = 'block'; }; document.body.appendChild(overlay); document.body.appendChild(modal); document.body.appendChild(settingsButton); // Event listener om de dropdown te enablen/disablen document.getElementById('setting-default-component').addEventListener('change', (e) => { document.getElementById('setting-component-choice').disabled = !e.target.checked; }); document.getElementById('save-settings-btn').addEventListener('click', () => { const newSettings = { highlightBugs: document.getElementById('setting-highlight-bugs').checked, setBillableToNo: document.getElementById('setting-set-billable').checked, setDefaultComponent: document.getElementById('setting-default-component').checked, defaultComponent: document.getElementById('setting-component-choice').value, setDefaultRegion: document.getElementById('setting-default-region').checked, }; saveSettings(newSettings); modal.style.display = 'none'; overlay.style.display = 'none'; alert('Instellingen opgeslagen. De pagina wordt herladen om de wijzigingen toe te passen.'); location.reload(); }); } /** * Injecteert een <style> tag in de <head> van de pagina met alle custom CSS. */ function injectCustomStyles(settings) { const styleId = 'jira-custom-card-styles'; if (document.getElementById(styleId)) { document.getElementById(styleId).remove(); // Verwijder oude stijl om bij te werken } const style = document.createElement('style'); style.id = styleId; // Bouw de CSS string op. Voeg bug-stijlen conditioneel toe. let css = ` .aui-label.ghx-title-label { background-color: #ffffff; color: #333; border: 1px solid #ccc; } .ghx-issue-compact.ghx-divider { height: auto !important; margin: 5px 0 !important; background: transparent !important; border: none !important; box-shadow: none !important; display: flex !important; align-items: center; justify-content: center; padding: 10px 0; position: relative; overflow: visible; cursor: move !important; } .ghx-issue-compact.ghx-divider::before { content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 1px; background-color: #ccc; z-index: 0; } .ghx-issue-compact.ghx-divider .ghx-row > *:not(.ghx-summary) { display: none !important; } .ghx-issue-compact.ghx-divider .ghx-summary { display: block !important; text-align: center; font-size: 14px; font-weight: bold; color: #707070; background-color: #f5f5f5; padding: 2px 15px; border-radius: 10px; position: relative; z-index: 1; flex-grow: 0 !important; opacity: 1 !important; } .ghx-issue-compact.ghx-divider .ghx-grabber, .ghx-issue-compact.ghx-divider .ghx-end, .ghx-issue-compact.ghx-divider .ghx-plan-extra-fields { display: none !important; } .ghx-issue-compact.ghx-divider .ghx-row { display: block !important; text-align: center; } .aui-label.ghx-time-critical-label { background-color: #ff5722; color: white; border-color: #e64a19; display: flex; align-items: center; gap: 5px; font-weight: bold; border-radius: 3px; } .ghx-cached-labels { margin-top: 5px; padding-left: 28px; display: flex; flex-wrap: wrap; gap: 5px; } .ghx-status-indicator { display: flex; flex-direction: column-reverse; gap: 1px; width: 8px; height: 11px; justify-content: center; flex-shrink: 0; margin: 0 2px; vertical-align: middle; } .ghx-status-indicator-bar { height: 3px; width: 100%; background-color: #fff; border: 1px solid #ccc; box-sizing: border-box; } .ghx-issue-compact.status-inprogress .ghx-status-indicator-bar:nth-child(-n+1) { background-color: #59afe1; border-color: #59afe1; } .ghx-issue-compact.status-test .ghx-status-indicator-bar:nth-child(-n+2) { background-color: #f6c342; border-color: #f6c342; } .ghx-issue-compact.status-done .ghx-status-indicator-bar, .ghx-issue-compact.status-closed .ghx-status-indicator-bar { background-color: #8eb021; border-color: #8eb021; } `; if (settings.highlightBugs) { css += ` .ghx-issue-compact.ghx-bug-card, .ghx-swimlane .ghx-issue.ghx-bug-card, .ghx-issue-compact.ghx-bug-card .ghx-end { background-color: #ffe7e7 !important; } `; } style.innerHTML = css; document.head.appendChild(style); } function createSizeIcon(sizeText) { const icon = document.createElement('div'); icon.textContent = sizeText; Object.assign(icon.style, { width: '24px', height: '24px', borderRadius: '50%', backgroundColor: '#f5f5f5', border: '1px solid #ccc', color: '#333', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 'bold', fontSize: '12px', flexShrink: '0', fontFamily: 'Arial, sans-serif' }); return icon; } const labelActions = { 'divider': { type: 'custom', handler: function(card) { if (card.classList.contains('ghx-divider')) return false; card.classList.add('ghx-divider'); const summary = card.querySelector('.ghx-summary'); if (summary) { summary.style.opacity = '1'; const inner = summary.querySelector('.ghx-inner'); if (inner && inner.dataset.originalTitle) { inner.innerHTML = inner.dataset.originalTitle; } } card.querySelectorAll('.ghx-plan-extra-fields, .ghx-end.ghx-row').forEach(el => el.remove()); return false; } }, 'p048': { type: 'style', styles: { backgroundColor: '#f79232', color: '#fff', borderColor: '#f79232' } }, 'urgent': { type: 'style', styles: { backgroundColor: '#d04437', color: 'white', borderColor: '#d04437' } }, 'on-hold': { type: 'custom', handler: function(card) { if (card.querySelector('.on-hold-label')) return false; const onHoldLabel = document.createElement('span'); onHoldLabel.textContent = `🛑 On Hold`; onHoldLabel.className = 'aui-label on-hold-label'; Object.assign(onHoldLabel.style, { marginLeft: '5px', flexShrink: '0' }); const keyElement = card.querySelector('.ghx-key'); const summaryElement = card.querySelector('.ghx-summary'); if (keyElement) keyElement.insertAdjacentElement('afterend', onHoldLabel); if (summaryElement) summaryElement.style.opacity = '0.6'; return false; } }, 'small': { type: 'custom', handler: function(card, labelText, rightContainer) { if (!card.querySelector('.size-icon-s')) { const icon = createSizeIcon('S'); icon.classList.add('size-icon-s'); rightContainer.appendChild(icon); } return false; } }, 'medium': { type: 'custom', handler: function(card, labelText, rightContainer) { if (!card.querySelector('.size-icon-m')) { const icon = createSizeIcon('M'); icon.classList.add('size-icon-m'); rightContainer.appendChild(icon); } return false; } }, 'large': { type: 'custom', handler: function(card, labelText, rightContainer) { if (!card.querySelector('.size-icon-l')) { const icon = createSizeIcon('L'); icon.classList.add('size-icon-l'); rightContainer.appendChild(icon); } return false; } }, 'onderzoek': { type: 'custom', handler: function(card, labelText, rightContainer) { if (card.querySelector('.research-label')) return false; const researchLabel = document.createElement('span'); researchLabel.className = 'aui-label research-label'; researchLabel.innerHTML = '🔍 Onderzoek'; Object.assign(researchLabel.style, { borderColor: '#3572b0', display: 'flex', alignItems: 'center', gap: '4px' }); rightContainer.appendChild(researchLabel); return false; } }, 'tijdskritisch': { type: 'custom', handler: function(card, labelText, rightContainer) { if (card.querySelector('.ghx-time-critical-label')) return false; const timeCriticalLabel = document.createElement('span'); timeCriticalLabel.className = 'aui-label ghx-time-critical-label'; timeCriticalLabel.innerHTML = '⏰ Tijdskritisch'; rightContainer.appendChild(timeCriticalLabel); return false; } }, 'release': { type: 'custom', handler: function(card, labelText, rightContainer) { if (card.querySelector('.release-label')) return false; const releaseLabel = document.createElement('span'); releaseLabel.className = 'aui-label release-label'; releaseLabel.innerHTML = '🔨 Uitvoeren release'; Object.assign(releaseLabel.style, { display: 'flex', alignItems: 'center', gap: '4px' }); rightContainer.appendChild(releaseLabel); return false; } } }; function processSingleCard(card, settings) { const labelTooltip = card.querySelector('span[data-tooltip^="Labels:"]'); if (labelTooltip && labelTooltip.dataset.tooltip.toLowerCase().includes('divider')) { labelActions['divider'].handler(card); card.classList.add('ghx-layout-processed'); return; } card.querySelector('.ghx-flags')?.remove(); const typeSpan = card.querySelector('.ghx-type'); if (settings.highlightBugs) { if (typeSpan && typeSpan.title.toLowerCase() === 'bug') { card.classList.add('ghx-bug-card'); } else { card.classList.remove('ghx-bug-card'); } } else { card.classList.remove('ghx-bug-card'); } const summaryElementReset = card.querySelector('.ghx-summary'); if (summaryElementReset) { summaryElementReset.style.opacity = '1'; const innerSpan = summaryElementReset.querySelector('.ghx-inner'); if (innerSpan && innerSpan.dataset.originalTitle) { innerSpan.innerHTML = innerSpan.dataset.originalTitle; } } const issueContent = card.querySelector('.ghx-issue-content'); if (!issueContent) return; const mainRow = issueContent.querySelector('.ghx-row'); if (!mainRow) return; mainRow.querySelectorAll('.on-hold-label, .ghx-status-indicator').forEach(el => el.remove()); Object.assign(mainRow.style, { display: 'flex', alignItems: 'center', gap: '5px' }); const masterRightContainer = document.createElement('span'); masterRightContainer.className = 'ghx-end'; Object.assign(masterRightContainer.style, { display: 'flex', alignItems: 'center', gap: '8px', marginLeft: 'auto', flexShrink: '0' }); const labelsSubContainer = document.createElement('span'); Object.assign(labelsSubContainer.style, { display: 'flex', alignItems: 'center', gap: '5px' }); const sizeIconsSubContainer = document.createElement('span'); Object.assign(sizeIconsSubContainer.style, { display: 'flex', alignItems: 'center', gap: '5px' }); const titleCategorySubContainer = document.createElement('span'); Object.assign(titleCategorySubContainer.style, { display: 'flex', alignItems: 'center', gap: '5px' }); const otherFieldsSubContainer = document.createElement('span'); Object.assign(otherFieldsSubContainer.style, { display: 'flex', alignItems: 'center', gap: '5px' }); const elementsToProcess = []; const containersToRemove = []; card.querySelectorAll('.ghx-plan-extra-fields, .ghx-issue-content > .ghx-end.ghx-row').forEach(container => { elementsToProcess.push(...container.children); containersToRemove.push(container); }); const statusField = elementsToProcess.find(field => field.matches('span[data-tooltip^="Status:"]')); if (statusField) { const statusText = (statusField.dataset.tooltip || '').replace('Status:', '').trim().toLowerCase(); const statusClasses = ['status-open', 'status-reopened', 'status-inprogress', 'status-test', 'status-closed', 'status-done']; card.classList.remove(...statusClasses); const statusMap = { 'open': { class: 'status-open', name: 'Open' }, 'reopened': { class: 'status-reopened', name: 'Reopened' }, 'in progress': { class: 'status-inprogress', name: 'In Progress' }, 'test': { class: 'status-test', name: 'Test' }, 'closed': { class: 'status-closed', name: 'Closed' }, 'done': { class: 'status-done', name: 'Done' } }; if (statusMap[statusText]) { card.classList.add(statusMap[statusText].class); const progressIndicator = document.createElement('div'); progressIndicator.className = 'ghx-status-indicator'; progressIndicator.title = `Status: ${statusMap[statusText].name}`; for (let i = 0; i < 3; i++) { const bar = document.createElement('div'); bar.className = 'ghx-status-indicator-bar'; progressIndicator.appendChild(bar); } if(typeSpan) typeSpan.insertAdjacentElement('afterend', progressIndicator); } } elementsToProcess.forEach(field => { if (field.matches('span[data-tooltip^="Status:"]') || field.classList.contains('ghx-extra-field-seperator')) return; if (field.matches('span[data-tooltip^="Labels:"]')) { const labelContentEl = field.querySelector('.ghx-extra-field-content'); const labels = (labelContentEl && labelContentEl.textContent.trim() !== 'None') ? labelContentEl.textContent.trim().split(/,\s*/).filter(Boolean) : []; const ticketKeyElement = card.querySelector('.ghx-key a'); if (ticketKeyElement) { const ticketKey = ticketKeyElement.textContent.trim(); const cache = getLabelCache(); if (JSON.stringify(cache[ticketKey]) !== JSON.stringify(labels)) { cache[ticketKey] = labels; saveLabelCache(cache); } } labels.forEach(label => { const labelLower = label.toLowerCase(); const action = labelActions[labelLower]; let addToRightAsDefault = true; if (action?.type === 'custom') { let targetContainer = labelsSubContainer; if (['small', 'medium', 'large'].includes(labelLower)) { targetContainer = sizeIconsSubContainer; } if (action.handler(card, label, targetContainer) === false) { addToRightAsDefault = false; } } if (addToRightAsDefault) { const lozenge = document.createElement('span'); lozenge.className = 'aui-label'; lozenge.textContent = label; if (action?.type === 'style') Object.assign(lozenge.style, action.styles); labelsSubContainer.appendChild(lozenge); } }); } else { otherFieldsSubContainer.appendChild(field.cloneNode(true)); } }); containersToRemove.forEach(container => container.remove()); mainRow.querySelector('.ghx-end')?.remove(); if (labelsSubContainer.hasChildNodes()) masterRightContainer.appendChild(labelsSubContainer); if (sizeIconsSubContainer.hasChildNodes()) masterRightContainer.appendChild(sizeIconsSubContainer); if (titleCategorySubContainer.hasChildNodes()) masterRightContainer.appendChild(titleCategorySubContainer); if (otherFieldsSubContainer.hasChildNodes()) masterRightContainer.appendChild(otherFieldsSubContainer); if (masterRightContainer.hasChildNodes()) { mainRow.appendChild(masterRightContainer); } card.classList.add('ghx-layout-processed'); } function processSwimlaneCards(settings) { const labelCache = getLabelCache(); const swimlaneCards = document.querySelectorAll('.ghx-swimlane .ghx-issue:not(.ghx-swimlane-processed)'); swimlaneCards.forEach(card => { const issueKey = card.dataset.issueKey; if (!issueKey) return; const typeSpan = card.querySelector('.ghx-type'); if (settings.highlightBugs) { if (typeSpan && typeSpan.title.toLowerCase() === 'bug') { card.classList.add('ghx-bug-card'); } else { card.classList.remove('ghx-bug-card'); } } else { card.classList.remove('ghx-bug-card'); } const cachedLabels = labelCache[issueKey]; if (cachedLabels && Array.isArray(cachedLabels) && cachedLabels.length > 0) { let labelContainer = card.querySelector('.ghx-cached-labels'); if (!labelContainer) { labelContainer = document.createElement('div'); labelContainer.className = 'ghx-cached-labels'; card.querySelector('.ghx-issue-fields')?.appendChild(labelContainer); } labelContainer.innerHTML = ''; card.style.opacity = '1'; cachedLabels.forEach(label => { const labelLower = label.toLowerCase(); if (labelLower === 'divider') return; const action = labelActions[labelLower]; const lozenge = document.createElement('span'); lozenge.className = 'aui-label'; lozenge.textContent = label; if (action?.type === 'style') { Object.assign(lozenge.style, action.styles); } if (labelLower === 'tijdskritisch') { lozenge.className = 'aui-label ghx-time-critical-label'; lozenge.innerHTML = '⏰ Tijdskritisch'; } else if (labelLower === 'onderzoek') { lozenge.innerHTML = '🔍 Onderzoek'; } else if (labelLower === 'release') { lozenge.innerHTML = '🔨 Uitvoeren release'; } else if (labelLower === 'on-hold') { lozenge.textContent = '🛑 On Hold'; card.style.opacity = '0.7'; } labelContainer.appendChild(lozenge); }); } card.classList.add('ghx-swimlane-processed'); }); } /** * Past het 'Create/Edit Issue' scherm aan op basis van instellingen. * @param {Object} settings - Het huidige instellingen-object. */ function enhanceCreateEditScreen(settings) { // 1. Facturabel if (settings.setBillableToNo) { const billableSelect = document.getElementById('customfield_10207'); if (billableSelect && !billableSelect.dataset.scriptProcessed) { const label = document.querySelector('label[for="customfield_10207"]'); const isRequired = label && label.querySelector('.icon-required'); if (isRequired && billableSelect.value === '') { const noOption = Array.from(billableSelect.options).find(opt => opt.text.trim() === 'No'); if (noOption) { billableSelect.value = noOption.value; billableSelect.dispatchEvent(new Event('change', { bubbles: true })); } } billableSelect.dataset.scriptProcessed = 'true'; } } // 2. Component if (settings.setDefaultComponent) { const componentsContainer = document.getElementById('components-multi-select'); if (componentsContainer && !componentsContainer.dataset.scriptProcessed) { const hasExistingComponent = componentsContainer.querySelector('.items li'); if (!hasExistingComponent) { const textarea = document.getElementById('components-textarea'); if (textarea) { textarea.focus(); textarea.value = settings.defaultComponent; textarea.dispatchEvent(new Event('input', { bubbles: true })); textarea.dispatchEvent(new Event('keyup', { bubbles: true })); setTimeout(() => { const suggestionsContainer = document.getElementById('components-suggestions'); if(suggestionsContainer) { const suggestionLink = Array.from(suggestionsContainer.querySelectorAll('li a')).find(a => a.title.trim() === settings.defaultComponent); if(suggestionLink) { suggestionLink.click(); } else { textarea.blur(); } } }, 250); } } componentsContainer.dataset.scriptProcessed = 'true'; } } // 3. Region if (settings.setDefaultRegion) { const regionSelect = document.getElementById('customfield_10210'); if (regionSelect && !regionSelect.dataset.scriptProcessed) { const isRequired = document.querySelector('label[for="customfield_10210"] .icon-required'); if (isRequired && regionSelect.value === '') { const algOption = Array.from(regionSelect.options).find(opt => opt.text.trim() === 'ALG'); if (algOption) { regionSelect.value = algOption.value; regionSelect.dispatchEvent(new Event('change', { bubbles: true })); } } regionSelect.dataset.scriptProcessed = 'true'; } } } /** * Zoekt naar onverwerkte elementen en voert de juiste processoren uit. */ function runAllProcessors(settings) { // Verwerk kaarten op het bord en in swimlanes const compactCards = document.querySelectorAll('.ghx-issue-compact:not(.ghx-layout-processed)'); if (compactCards.length > 0) { compactCards.forEach(card => processSingleCard(card, settings)); } processSwimlaneCards(settings); // Verwerk velden op het create/edit scherm enhanceCreateEditScreen(settings); } // --- Script Initialisatie --- const currentSettings = loadSettings(); injectCustomStyles(currentSettings); setupSettingsUI(); const observer = new MutationObserver(() => { clearTimeout(observer.timeout); // Laad de instellingen opnieuw voor het geval ze in een ander tabblad zijn gewijzigd. observer.timeout = setTimeout(() => runAllProcessors(loadSettings()), 100); }); observer.observe(document.body, { childList: true, subtree: true }); setTimeout(() => runAllProcessors(currentSettings), 500); })();