// ==UserScript== // @name Userscript Dev Toolkit Pro // @namespace https://github.com/SysAdminDoc/Userscript-Dev-Toolkit // @version 9.0.2 // @description A powerhouse side-panel for live userscript development. Press Ctrl+Shift+D to toggle. Features an advanced DOM inspector, form/storage analysis, resource filtering, and a premium UI. // @author Matthew Parker // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_info // @grant GM_download // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_listValues // @run-at document-idle // @license MIT // @updateURL https://github.com/SysAdminDoc/Userscript-Dev-Toolkit/blob/main/Userscript%20Dev%20Toolkit.user.js // @downloadURL https://github.com/SysAdminDoc/Userscript-Dev-Toolkit/blob/main/Userscript%20Dev%20Toolkit.user.js // ==/UserScript== (function() { 'use strict'; // --- CONFIG & HELPERS --- const KEYBOARD_SHORTCUT = { key: 'D', ctrlKey: true, shiftKey: true, altKey: false }; const qs = (selector, parent = document) => parent.querySelector(selector); const qsa = (selector, parent = document) => parent.querySelectorAll(selector); const clamp = (val, min, max) => Math.max(min, Math.min(val, max)); const clearElement = (el) => { if(el) while (el.firstChild) { el.removeChild(el.firstChild); } }; const createElement = (tag, classes = [], attributes = {}, text = '') => { const el = document.createElement(tag); if (classes.length) el.classList.add(...classes.filter(Boolean)); Object.entries(attributes).forEach(([key, value]) => el.setAttribute(key, value)); if (text) el.textContent = text; el.setAttribute('data-udt-element', 'true'); return el; }; const ICONS = { launcher: ``, sun: ``, moon: ``, copy: ``, hide: ``, info: ``, unhide: ``, settings: ``, pin: ``, arrowLeft: ``, arrowRight: ``, inspect: ``, }; class DevToolkit { constructor() { this.id = 'userscript-dev-toolkit-pro'; this.isPanelOpen = false; this.isPickerActive = false; this.isDragging = false; this.onPickerClickCallback = null; this.lastHighlightedElement = null; this.toastTimeout = null; this.launcherPos = { x: 30, y: 30 }; this.HIDDEN_SELECTORS_KEY = 'udt_hidden_selectors_v6'; this.dom = {}; // To store references to key DOM elements this.settings = new SettingsManager(this); this.components = {}; this.componentDefinitions = [ InspectorComponent, ElementScannerComponent, FormsComponent, ResourceViewerComponent, StorageComponent, uBlockComponent, AIComponent, HtmlStripperComponent, CSSViewerComponent, CSSToolsComponent, DebuggerComponent, ]; } async init() { if (window.top !== window.self) return; this.registerComponents(); await this.settings.load(); this.injectCoreMarkup(); this.storeDomReferences(); this.injectStyles(); this.renderTabs(); this.renderAllContent(); this.applyPreferences(); this.bindCoreEvents(); this.showToast(`Dev Toolkit Pro v9.0.2 Initialized.`, 2000); GM_registerMenuCommand('Toggle Dev Toolkit', () => this.handleShortcutToggle()); } injectCoreMarkup() { const launcher = createElement('div', [], { id: `${this.id}-launcher` }); launcher.innerHTML = ICONS.launcher; const panel = createElement('div', ['udt-panel'], { id: `${this.id}-panel` }); panel.innerHTML = `
Dev Toolkit Pro
${ICONS.pin}
${ICONS.sun}
${ICONS.moon}
${ICONS.settings}
`; const resizer = createElement('div', [], { id: `${this.id}-resizer` }); const highlighter = createElement('div', [], { id: `${this.id}-highlighter`, style: 'display: none;' }); const toastContainer = createElement('div', [], { id: `${this.id}-toast-container` }); document.body.append(launcher, panel, resizer, highlighter, toastContainer); } storeDomReferences() { this.dom = { launcher: qs(`#${this.id}-launcher`), panel: qs(`#${this.id}-panel`), resizer: qs(`#${this.id}-resizer`), pinBtn: qs(`#${this.id}-pin-btn`), themeToggle: qs(`#${this.id}-theme-toggle`), settingsBtn: qs(`#${this.id}-settings-btn`), sidebar: qs(`#${this.id}-sidebar`), contentArea: qs(`#${this.id}-content-area`), settingsPanel: qs(`#${this.id}-settings-panel`), highlighter: qs(`#${this.id}-highlighter`), toastContainer: qs(`#${this.id}-toast-container`), sideSwitcherBtn: qs(`#${this.id}-side-switcher-btn`) }; } injectStyles() { GM_addStyle(` :root { --udt-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; --udt-border-radius: 12px; --udt-transition-fast: all 0.2s ease-in-out; --udt-panel-width: 450px; --udt-table-cell-padding: 8px 12px; } html { transition: margin 0.3s ease-in-out; } body.udt-dark-theme { --bg-primary: #121212; --bg-secondary: #1e1e1e; --bg-tertiary: #2a2a2a; --bg-interactive: #333333; --bg-interactive-hover: #404040; --text-primary: #e0e0e0; --text-secondary: #b3b3b3; --border-primary: #383838; --accent-primary: #00aaff; --accent-glow: rgba(0, 170, 255, 0.3); --destructive: #ff4d4d; --destructive-glow: rgba(255, 77, 77, 0.3); --shadow-color: rgba(0, 0, 0, 0.5); } body.udt-light-theme { --bg-primary: #f5f5f7; --bg-secondary: #ffffff; --bg-tertiary: #f0f0f0; --bg-interactive: #e9e9e9; --bg-interactive-hover: #e0e0e0; --text-primary: #1d1d1f; --text-secondary: #515154; --border-primary: #d2d2d7; --accent-primary: #007aff; --accent-glow: rgba(0, 122, 255, 0.2); --destructive: #ff3b30; --destructive-glow: rgba(255, 59, 48, 0.2); --shadow-color: rgba(0, 0, 0, 0.15); } #${this.id}-launcher { position: fixed; width: 48px; height: 48px; background-color: var(--bg-secondary); border: 1px solid var(--border-primary); border-radius: 50%; box-shadow: 0 4px 15px var(--shadow-color); display: flex; align-items: center; justify-content: center; cursor: pointer; z-index: 2147483645; transition: transform 0.2s ease, box-shadow 0.2s ease; } #${this.id}-launcher:hover { transform: scale(1.1); box-shadow: 0 6px 20px var(--shadow-color), 0 0 15px var(--accent-glow); } #${this.id}-launcher svg { width: 28px; height: 28px; color: var(--accent-primary); transition: transform 0.8s ease; } .udt-panel { position: fixed; top: 0; height: 100vh; width: var(--udt-panel-width); z-index: 2147483646; display: flex; flex-direction: column; background-color: var(--bg-secondary); color: var(--text-primary); box-shadow: 0 0 20px var(--shadow-color); transition: transform 0.3s ease-in-out; } .udt-panel.side-left { left: 0; transform: translateX(-100%); border-right: 1px solid var(--border-primary); } .udt-panel.side-right { right: 0; transform: translateX(100%); border-left: 1px solid var(--border-primary); } .udt-panel.expanded { transform: translateX(0); } #${this.id}-resizer { position: fixed; top: 0; height: 100%; width: 5px; cursor: col-resize; z-index: 2147483647; } .udt-panel.side-left + #${this.id}-resizer { left: var(--udt-panel-width); } .udt-panel.side-right + #${this.id}-resizer { right: var(--udt-panel-width); } #${this.id}-header { display: flex; align-items: center; padding: 12px 20px; border-bottom: 1px solid var(--border-primary); flex-shrink: 0; } #${this.id}-title { font-size: 18px; font-weight: 600; margin-right: auto; white-space: nowrap; } .udt-header-icon { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; margin-left: 8px; cursor: pointer; color: var(--text-secondary); border-radius: 50%; transition: var(--udt-transition-fast); } .udt-panel.side-left #${this.id}-side-switcher-btn { order: 10; } /* Push to the right */ .udt-header-icon:hover { color: var(--text-primary); background-color: var(--bg-interactive); transform: scale(1.1); } .udt-header-icon svg { width: 20px; height: 20px; } .udt-header-icon#${this.id}-pin-btn.pinned { color: var(--accent-primary); } .udt-header-icon #udt-theme-icon-sun { display: none; } body.udt-dark-theme .udt-header-icon #udt-theme-icon-moon { display: none; } body.udt-dark-theme .udt-header-icon #udt-theme-icon-sun { display: block; } #${this.id}-main-content { flex-grow: 1; display: flex; overflow: hidden; } #${this.id}-sidebar { width: 220px; flex-shrink: 0; padding: 12px 0; border-right: 1px solid var(--border-primary); display: flex; flex-direction: column; } .udt-panel.side-right #${this.id}-sidebar { order: 2; border-right: none; border-left: 1px solid var(--border-primary); } .udt-tab-btn { width: 100%; padding: 10px 20px; font-size: 15px; font-weight: 500; cursor: pointer; background: none; border: none; color: var(--text-secondary); text-align: left; border-left: 3px solid transparent; transition: var(--udt-transition-fast); display: flex; align-items: center; gap: 12px; } .udt-tab-btn:hover { color: var(--text-primary); background-color: var(--bg-tertiary); } .udt-tab-btn.active { color: var(--accent-primary); font-weight: 600; background-color: var(--bg-tertiary); border-left-color: var(--accent-primary); } .udt-panel.side-right .udt-tab-btn { border-left: none; border-right: 3px solid transparent; } .udt-panel.side-right .udt-tab-btn.active { border-right-color: var(--accent-primary); } .udt-tab-btn .picker-active-dot { width: 8px; height: 8px; background-color: var(--destructive); border-radius: 50%; animation: udt-pulse 1.5s infinite; } #${this.id}-content-area { flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; position: relative; } .udt-content-panel { position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; flex-direction: column; opacity: 0; visibility: hidden; transition: opacity 0.2s ease, visibility 0s 0.2s; } .udt-content-panel.active { opacity: 1; visibility: visible; z-index: 1; transition: opacity 0.2s ease 0.1s, visibility 0s; } .udt-panel-header { padding: 16px 24px; border-bottom: 1px solid var(--border-primary); display: flex; align-items: center; gap: 16px; flex-shrink: 0; } .udt-panel-title { font-size: 18px; font-weight: 600; color: var(--text-primary); margin-right:auto; } .udt-panel-body { flex-grow: 1; overflow-y: auto; padding: 15px; font-size: 14px; } .udt-panel-body.no-padding { padding: 0; } .udt-panel-body::-webkit-scrollbar { width: 12px; } .udt-panel-body::-webkit-scrollbar-track { background: var(--bg-tertiary); } .udt-panel-body::-webkit-scrollbar-thumb { background-color: var(--bg-interactive-hover); border-radius: 10px; border: 3px solid var(--bg-tertiary); } .udt-btn { padding: 8px 16px; font-size: 14px; font-weight: 600; border: none; border-radius: 8px; cursor: pointer; transition: var(--udt-transition-fast); display: inline-flex; align-items: center; gap: 8px; } .udt-btn.primary { color: #fff; background-image: linear-gradient(to top, var(--accent-primary), #00c6ff); box-shadow: 0 4px 15px var(--accent-glow); } .udt-btn.primary:hover { transform: translateY(-2px); box-shadow: 0 6px 20px var(--accent-glow); } .udt-btn.secondary { background-color: var(--bg-interactive); color: var(--text-primary); } .udt-btn.secondary:hover { background-color: var(--bg-interactive-hover); } .udt-btn.destructive { background-color: var(--bg-interactive); color: var(--destructive); } .udt-btn.destructive:hover { background-color: var(--destructive-glow); } .udt-input, .udt-textarea, .udt-select { width: 100%; background-color: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-primary); padding: 8px; border-radius: 6px; font-family: var(--udt-font-family); } .udt-input-group { display: flex; } .udt-input-group > .udt-input { border-right: 0; border-top-right-radius: 0; border-bottom-right-radius: 0; } .udt-input-group > .udt-copy-btn { border-top-left-radius: 0; border-bottom-left-radius: 0; } .udt-table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; } .udt-table th { position: sticky; top: 0; background-color: var(--bg-secondary); z-index: 1; cursor: pointer; user-select: none; position: relative; } .udt-table th .resize-handle { position: absolute; top: 0; right: 0; width: 5px; height: 100%; cursor: col-resize; z-index: 2; } .udt-table th, .udt-table td { padding: var(--udt-table-cell-padding); text-align: left; border-bottom: 1px solid var(--border-primary); white-space:nowrap; overflow: hidden; text-overflow: ellipsis; } .udt-table td { max-width: 200px; } /* Prevent wide columns by default */ .udt-table tbody tr:hover { background-color: var(--bg-tertiary); } .udt-action-icon { cursor: pointer; color: var(--text-secondary); width: 18px; height: 18px; transition: color 0.2s, transform 0.2s; } .udt-action-icon.copy:hover, .udt-action-icon.inspect:hover { color: var(--accent-primary); transform: scale(1.2); } .udt-action-icon.hide:hover { color: var(--destructive); transform: scale(1.2); } #${this.id}-highlighter { position: absolute; background-color: rgba(255, 0, 255, 0.2); border: 2px dashed #ff00ff; border-radius: 4px; z-index: 2147483644; pointer-events: none; transition: all 0.1s ease-out; box-sizing: border-box; } .element-selected-permanent { outline: 3px solid var(--accent-primary) !important; box-shadow: 0 0 12px var(--accent-glow) !important; } .udt-settings-group { margin-bottom: 24px; padding-bottom: 24px; border-bottom: 1px solid var(--border-primary); } .udt-settings-group:last-child { border-bottom: none; } .udt-settings-group h3 { font-size: 16px; font-weight: 600; margin-bottom: 16px; } .udt-settings-row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; } .udt-toggle-switch { position: relative; display: inline-block; width: 50px; height: 28px; } .udt-toggle-switch input { display: none; } .udt-toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--bg-interactive); transition: var(--udt-transition-fast); border-radius: 28px; } .udt-toggle-slider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 4px; bottom: 4px; background-color: white; transition: var(--udt-transition-fast); border-radius: 50%; } input:checked + .udt-toggle-slider { background-color: var(--accent-primary); } input:checked + .udt-toggle-slider:before { transform: translateX(22px); } #udt-hidden-items-list { list-style: none; padding: 0; max-height: 200px; overflow-y: auto; border: 1px solid var(--border-primary); border-radius: 8px; } .udt-hidden-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; font-family: 'Courier New', monospace; font-size: 13px; border-bottom: 1px solid var(--border-primary); } .udt-hidden-item:last-child { border-bottom: none; } .udt-unhide-btn { display: flex; align-items: center; gap: 6px; cursor: pointer; color: var(--text-secondary); transition: var(--udt-transition-fast); background:none; border:none; font-size:13px; font-family:var(--udt-font-family); } .udt-unhide-btn:hover { color: var(--accent-primary); } .udt-unhide-btn svg { width: 16px; height: 16px; } #${this.id}-toast-container { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); z-index: 2147483647; display: flex; flex-direction: column; gap: 10px; } .udt-toast { padding: 12px 20px; background-color: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-primary); border-radius: 8px; box-shadow: 0 4px 15px var(--shadow-color); font-size: 14px; font-weight: 500; display: flex; align-items: center; gap: 10px; animation: udt-toast-in 0.3s ease forwards, udt-toast-out 0.3s ease 2.7s forwards; } .udt-toast svg { width: 20px; height: 20px; color: var(--accent-primary); } @keyframes udt-toast-in { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } @keyframes udt-toast-out { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(20px); } } @keyframes udt-pulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(0.8); } } `); } renderTabs() { clearElement(this.dom.sidebar); this.getVisibleComponents().forEach(comp => { const tabBtn = createElement('button', ['udt-tab-btn'], { 'data-tab': comp.id, title: comp.name }); tabBtn.innerHTML = `${comp.icon || '●'}${comp.name}`; this.dom.sidebar.appendChild(tabBtn); }); } renderAllContent() { this.getEnabledComponents().forEach(comp => { const contentEl = comp.render(); this.dom.contentArea.appendChild(contentEl); }); this.renderSettingsMenu(); } renderSettingsMenu() { const menu = this.dom.settingsPanel; clearElement(menu); const body = createElement('div', ['udt-panel-body']); const header = createElement('div', ['udt-panel-header']); header.append(createElement('h2', ['udt-panel-title'], {}, 'Settings')); const appearanceGroup = createElement('div', ['udt-settings-group']); appearanceGroup.append(createElement('h3', [], {}, 'Appearance')); const darkModeRow = createElement('div', ['udt-settings-row']); darkModeRow.append(createElement('label', [], {}, 'Dark Mode'), this.createToggle('dark-mode-toggle', this.settings.get('ui.isDarkMode'))); const sideSelectRow = createElement('div', ['udt-settings-row']); const sideSelect = createElement('select', ['udt-select'], {id: 'udt-side-select'}); sideSelect.innerHTML = ``; sideSelect.value = this.settings.get('ui.positionSide'); sideSelectRow.append(createElement('label', [], {}, 'Panel Side'), sideSelect); appearanceGroup.append(darkModeRow, sideSelectRow); const tabsGroup = createElement('div', ['udt-settings-group']); tabsGroup.append(createElement('h3', [], {}, 'Visible Tabs')); this.getOrderedComponents().forEach(comp => { const isVisible = this.settings.getComponentState(comp.id)?.showInToolbar ?? true; const row = createElement('div', ['udt-settings-row']); row.append(createElement('label', [], {}, comp.name), this.createToggle(null, isVisible, { 'data-component-id': comp.id })); tabsGroup.append(row); }); const dataGroup = createElement('div', ['udt-settings-group']); dataGroup.innerHTML = `

Hidden Items on ${window.location.hostname}

`; const exportGroup = createElement('div', ['udt-settings-group']); exportGroup.innerHTML = `

Data

`; const exportRow = createElement('div', ['udt-settings-row']); exportRow.append(createElement('label', [], {}, 'Export all data to a JSON file.'), createElement('button', ['udt-btn', 'primary'], { id: 'udt-export-btn' }, 'Export All Data')); exportGroup.append(exportRow); body.append(appearanceGroup, tabsGroup, dataGroup, exportGroup); menu.append(header, body); this.bindSettingsMenuEvents(); } createToggle(id, isChecked, attributes = {}) { const label = createElement('label', ['udt-toggle-switch']); const input = createElement('input', [], { type: 'checkbox', ...attributes }); if (id) input.id = id; input.checked = isChecked; const slider = createElement('span', ['udt-toggle-slider']); label.append(input, slider); return label; } registerComponents() { this.componentDefinitions.forEach(CompClass => { const instance = new CompClass(this); this.components[instance.id] = instance; }); } getOrderedComponents() { const order = this.settings.get('components.order') || []; const componentMap = new Map(Object.values(this.components).map(c => [c.id, c])); return [...order.map(id => componentMap.get(id)).filter(Boolean), ...Object.values(this.components).filter(c => !order.includes(c.id))]; } getEnabledComponents() { return this.getOrderedComponents().filter(c => this.settings.getComponentState(c.id)?.enabled); } getVisibleComponents() { return this.getOrderedComponents().filter(c => this.settings.getComponentState(c.id)?.showInToolbar); } applyPreferences() { this.applyTheme(this.settings.get('ui.isDarkMode')); this.switchSide(this.settings.get('ui.positionSide')); const isPinned = this.settings.get('ui.isPinned'); if (isPinned) { this.dom.panel.classList.add('expanded'); this.updatePageMargin(true); } if (this.dom.pinBtn) this.dom.pinBtn.classList.toggle('pinned', isPinned); (async () => { this.launcherPos = await GM_getValue('udtLauncherPos_v7', { x: 30, y: 30 }); this.dom.launcher.style.left = `${this.launcherPos.x}px`; this.dom.launcher.style.top = `${this.launcherPos.y}px`; const panelWidth = await GM_getValue('udtPanelWidth_v7', 450); document.documentElement.style.setProperty('--udt-panel-width', `${panelWidth}px`); })(); } applyTheme(isDark) { const themeClass = isDark ? 'udt-dark-theme' : 'udt-light-theme'; document.body.classList.remove('udt-dark-theme', 'udt-light-theme'); document.body.classList.add(themeClass); this.settings.set('ui.isDarkMode', isDark); const toggle = qs('#dark-mode-toggle'); if (toggle) toggle.checked = isDark; } togglePanel() { const isPinned = this.settings.get('ui.isPinned'); if (isPinned) return; this.isPanelOpen = !this.isPanelOpen; this.dom.panel.classList.toggle('expanded', this.isPanelOpen); if (this.isPanelOpen) this.setActiveTab(this.settings.get('activeTab')); } handleShortcutToggle() { this.togglePin(); } togglePin() { const isPinned = !this.settings.get('ui.isPinned'); this.settings.set('ui.isPinned', isPinned); if(this.dom.pinBtn) this.dom.pinBtn.classList.toggle('pinned', isPinned); this.dom.panel.classList.toggle('expanded', isPinned); this.isPanelOpen = isPinned; this.updatePageMargin(isPinned); } updatePageMargin(isPanelVisible) { const side = this.settings.get('ui.positionSide'); const width = isPanelVisible ? `calc(${getComputedStyle(document.documentElement).getPropertyValue('--udt-panel-width')} + 5px)` : '0px'; document.documentElement.style.marginLeft = (side === 'left') ? width : '0px'; document.documentElement.style.marginRight = (side === 'right') ? width : '0px'; } switchSide(side) { this.dom.panel.classList.remove('side-left', 'side-right'); this.dom.panel.classList.add(`side-${side}`); this.dom.resizer.style.left = side === 'left' ? 'var(--udt-panel-width)' : 'auto'; this.dom.resizer.style.right = side === 'right' ? 'var(--udt-panel-width)' : 'auto'; this.settings.set('ui.positionSide', side); this.updatePageMargin(this.settings.get('ui.isPinned')); this.updateSideSwitcherIcon(); } updateSideSwitcherIcon() { if (!this.dom.sideSwitcherBtn) return; const side = this.settings.get('ui.positionSide'); this.dom.sideSwitcherBtn.innerHTML = side === 'left' ? ICONS.arrowRight : ICONS.arrowLeft; this.dom.sideSwitcherBtn.title = side === 'left' ? 'Dock Right' : 'Dock Left'; } setActiveTab(tabId) { if (tabId === 'settings') { this.populateHiddenElementsList(); } else { const comp = this.components[tabId]; if (!comp || !this.settings.getComponentState(tabId)?.showInToolbar) { tabId = this.getVisibleComponents()[0]?.id; } } if (!tabId) return; qsa('.udt-tab-btn', this.dom.panel).forEach(btn => btn.classList.toggle('active', btn.dataset.tab === tabId)); qsa('.udt-content-panel', this.dom.panel).forEach(content => content.classList.toggle('active', content.id === `${this.id}-${tabId}-panel`)); this.settings.set('activeTab', tabId); this.components[tabId]?.update?.(); } showToast(message, duration = 3000) { const toast = createElement('div', ['udt-toast']); toast.innerHTML = `${ICONS.info} ${message}`; this.dom.toastContainer.appendChild(toast); clearTimeout(this.toastTimeout); this.toastTimeout = setTimeout(() => toast.remove(), duration); } startPicker(callback, cursor = 'crosshair', highlight = true, sourceComponent) { if (this.isPickerActive) return; this.isPickerActive = true; this.onPickerClickCallback = callback; document.body.style.cursor = cursor; this.onPickerHover = (e) => { if (e.target.closest('[data-udt-element]')) { this.removeHighlight(); return; } if (e.target === this.lastHighlightedElement) return; if (highlight) { this.removeHighlight(); this.lastHighlightedElement = e.target; this.dom.highlighter.style.display = 'block'; this.updateHighlighter(); } }; this.onPickerClick = (e) => { if (e.target.closest('[data-udt-element]')) return; e.preventDefault(); e.stopPropagation(); if (this.onPickerClickCallback) this.onPickerClickCallback(e.target); this.stopPicker(); }; document.addEventListener('mousemove', this.onPickerHover, true); document.addEventListener('click', this.onPickerClick, true); sourceComponent?.updatePickerButton(true); } stopPicker() { if (!this.isPickerActive) return; this.isPickerActive = false; document.body.style.cursor = ''; document.removeEventListener('mousemove', this.onPickerHover, true); document.removeEventListener('click', this.onPickerClick, true); this.removeHighlight(); Object.values(this.components).forEach(c => c.updatePickerButton?.(false)); } removeHighlight() { if(this.dom.highlighter) this.dom.highlighter.style.display = 'none'; if (this.lastHighlightedElement) { this.lastHighlightedElement = null; } } updateHighlighter() { if (!this.lastHighlightedElement) return; const rect = this.lastHighlightedElement.getBoundingClientRect(); const highlighter = this.dom.highlighter; highlighter.style.width = `${rect.width}px`; highlighter.style.height = `${rect.height}px`; highlighter.style.top = `${rect.top + window.scrollY}px`; highlighter.style.left = `${rect.left + window.scrollX}px`; } bindCoreEvents() { this.dom.launcher.addEventListener('click', () => { if(!this.isDragging) { this.dom.panel.classList.add('expanded'); this.isPanelOpen = true; } }); this.dom.panel.addEventListener('mouseenter', () => { if (!this.settings.get('ui.isPinned')) { this.dom.panel.classList.add('expanded'); this.isPanelOpen = true; } }); this.dom.panel.addEventListener('mouseleave', () => { if (!this.settings.get('ui.isPinned')) { this.dom.panel.classList.remove('expanded'); this.isPanelOpen = false; } }); if (this.dom.pinBtn) this.dom.pinBtn.addEventListener('click', () => this.togglePin()); if (this.dom.sideSwitcherBtn) this.dom.sideSwitcherBtn.addEventListener('click', () => this.switchSide(this.settings.get('ui.positionSide') === 'left' ? 'right' : 'left')); if (this.dom.sidebar) this.dom.sidebar.addEventListener('click', e => { const tabId = e.target.closest('.udt-tab-btn')?.dataset.tab; if (tabId) this.setActiveTab(tabId); }); if (this.dom.settingsBtn) this.dom.settingsBtn.addEventListener('click', () => this.setActiveTab('settings')); if (this.dom.themeToggle) this.dom.themeToggle.addEventListener('click', () => this.applyTheme(!this.settings.get('ui.isDarkMode'))); this.dom.panel.addEventListener('click', e => { const copyBtn = e.target.closest('.udt-copy-btn'); if (copyBtn) { const input = copyBtn.previousElementSibling; if (input) { GM_setClipboard(input.value); this.showToast('Copied!'); } } }); document.addEventListener('keydown', e => { if (e.key.toUpperCase() === KEYBOARD_SHORTCUT.key && e.ctrlKey === KEYBOARD_SHORTCUT.ctrlKey && e.shiftKey === KEYBOARD_SHORTCUT.shiftKey && e.altKey === KEYBOARD_SHORTCUT.altKey) { e.preventDefault(); e.stopPropagation(); this.handleShortcutToggle(); } }); this.bindDragEvents(this.dom.launcher, 'udtLauncherPos_v7'); this.bindResizeEvents(); } bindSettingsMenuEvents() { const settingsPanel = this.dom.settingsPanel; if (!settingsPanel) return; qs('#dark-mode-toggle', settingsPanel)?.addEventListener('change', e => this.applyTheme(e.target.checked)); qs('#udt-side-select', settingsPanel)?.addEventListener('change', e => this.switchSide(e.target.value)); qsa('[data-component-id]', settingsPanel).forEach(toggle => { toggle.addEventListener('change', e => { const id = e.target.dataset.componentId; this.settings.setComponentState(id, { ...this.settings.getComponentState(id), showInToolbar: e.target.checked }); this.renderTabs(); }); }); qs('#udt-clear-hidden-btn', settingsPanel)?.addEventListener('click', async () => { await this.setHiddenSelectors([]); await this.populateHiddenElementsList(); this.showToast('All hidden items cleared for this domain.'); }); qs('#udt-export-btn', settingsPanel)?.addEventListener('click', () => this.exportDataAsJSON()); qs('#udt-hidden-items-list', settingsPanel)?.addEventListener('click', async (e) => { const unhideBtn = e.target.closest('.udt-unhide-btn'); if (!unhideBtn) return; const item = unhideBtn.closest('.udt-hidden-item'); const selectorToUnhide = item.dataset.selector; let hiddenSelectors = await this.getHiddenSelectors(); const updatedSelectors = hiddenSelectors.filter(s => s !== selectorToUnhide); await this.setHiddenSelectors(updatedSelectors); item.remove(); this.showToast('Element un-hidden. Rescan to see it.'); }); } bindResizeEvents() { const resizer = this.dom.resizer; if (!resizer) return; const onMouseDown = (e) => { e.preventDefault(); const startX = e.clientX; const startWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--udt-panel-width')); const side = this.settings.get('ui.positionSide'); const onMouseMove = (moveEvent) => { const dx = moveEvent.clientX - startX; const newWidth = clamp(side === 'left' ? startWidth + dx : startWidth - dx, 300, 1000); document.documentElement.style.setProperty('--udt-panel-width', `${newWidth}px`); if (this.settings.get('ui.isPinned')) this.updatePageMargin(true); }; const onMouseUp = () => { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); GM_setValue('udtPanelWidth_v7', parseInt(getComputedStyle(document.documentElement).getPropertyValue('--udt-panel-width'))); }; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; resizer.addEventListener('mousedown', onMouseDown); } bindDragEvents(element, storageKey) { if (!element) return; let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; element.onmousedown = (e) => { e = e || window.event; e.preventDefault(); this.isDragging = false; pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; document.onmousemove = elementDrag; }; const elementDrag = (e) => { this.isDragging = true; e = e || window.event; e.preventDefault(); pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; element.style.top = (element.offsetTop - pos2) + "px"; element.style.left = (element.offsetLeft - pos1) + "px"; element.style.cursor = 'grabbing'; }; const closeDragElement = () => { document.onmouseup = null; document.onmousemove = null; element.style.cursor = 'grab'; this.launcherPos = { x: element.offsetLeft, y: element.offsetTop }; GM_setValue(storageKey, this.launcherPos); setTimeout(() => this.isDragging = false, 50); }; } getHiddenSelectors = async () => { const all = await GM_getValue(this.HIDDEN_SELECTORS_KEY, {}); return all[window.location.hostname] || []; }; setHiddenSelectors = async (selectors) => { const all = await GM_getValue(this.HIDDEN_SELECTORS_KEY, {}); all[window.location.hostname] = selectors; await GM_setValue(this.HIDDEN_SELECTORS_KEY, all); }; async populateHiddenElementsList() { const list = qs('#udt-hidden-items-list', this.dom.panel); if (!list) return; clearElement(list); const hiddenSelectors = await this.getHiddenSelectors(); if (hiddenSelectors.length === 0) { list.append(createElement('li', ['udt-hidden-item'], {}, 'No items hidden on this domain.')); return; } hiddenSelectors.forEach(selector => { const item = createElement('li', ['udt-hidden-item'], { 'data-selector': selector }); item.append(createElement('span', ['udt-hidden-item-selector'], {title: selector}, selector)); const btn = createElement('button', ['udt-unhide-btn']); btn.innerHTML = `${ICONS.unhide} Unhide`; item.append(btn); list.append(item); }); } async exportDataAsJSON() { this.showToast("Generating export..."); const interactiveElements = Array.from(document.querySelectorAll('button, a[href], input:not([type="hidden"]), select, textarea')) .filter(el => !el.closest('[data-udt-element]')) .map(el => ({ tag: el.tagName.toLowerCase(), text: el.textContent?.trim() || el.name || el.value || '', selector: getCssPath(el), html: el.outerHTML })); const resources = performance.getEntriesByType("resource").map(res => ({ name: res.name, type: res.initiatorType, size_kb: (res.transferSize / 1024).toFixed(2), load_time_ms: res.duration.toFixed(2) })); const exportData = { metadata: { url: window.location.href, domain: window.location.hostname, exportedAt: new Date().toISOString(), hiddenSelectorsOnDomain: await this.getHiddenSelectors() }, interactiveElements, resources }; GM_download({ url: 'data:application/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(exportData, null, 2)), name: `udt-export-${window.location.hostname}-${new Date().toISOString().split('T')[0]}.json`, saveAs: true }); this.showToast("Export download started."); } } class ToolkitComponent { constructor(toolkit, id, name, icon = '●') { this.toolkit = toolkit; this.id = id; this.name = name; this.icon = icon; this.el = null; } render() { if (!this.el) { this.el = createElement('div', ['udt-content-panel'], { id: `${this.toolkit.id}-${this.id}-panel` }); this.buildContent(); this.bindEvents(); } return this.el; } buildContent() { this.el.innerHTML = `

${this.name}

Component not implemented.

`; } bindEvents() {} update() {} updatePickerButton(isPickerActive, btnClass = '.picker-btn') { const btn = qs(btnClass, this.el); if (btn) { btn.textContent = isPickerActive ? 'Cancel Picking' : btn.dataset.defaultText || 'Pick'; btn.classList.toggle('primary', !isPickerActive); const tabBtn = qs(`.udt-tab-btn[data-tab="${this.id}"]`, this.toolkit.dom.panel); if(tabBtn) { let dot = qs('.picker-active-dot', tabBtn); if (isPickerActive && !dot) { const iconSpan = qs('span:first-child', tabBtn); if(iconSpan) iconSpan.appendChild(createElement('span', ['picker-active-dot'])); } else if (!isPickerActive && dot) { dot.remove(); } } } } createInputGroup(labelText, inputId, inputValue, withCopy = true) { const container = createElement('div', [], {style: 'padding: 0; border: 0; margin: 0 0 10px 0;'}); const label = createElement('label', [], { for: inputId, style: 'display:block; margin-bottom: 4px; font-size: 12px; font-weight: 500; color: var(--text-secondary);' }, labelText); const group = createElement('div', ['udt-input-group']); const input = createElement('input', ['udt-input'], { id: inputId, value: inputValue, readonly: true, style: 'font-family: monospace; font-size: 12px;' }); group.append(input); if (withCopy) { const copyBtn = createElement('button', ['udt-btn', 'secondary', 'udt-copy-btn'], {title: 'Copy'}); copyBtn.innerHTML = ICONS.copy; group.append(copyBtn); } container.append(label, group); return container; } } // --- All Component Classes (ElementScanner, ResourceViewer, AI, Inspector, etc.) are defined below --- class ElementScannerComponent extends ToolkitComponent { constructor(toolkit) { super(toolkit, 'element-scanner', 'Scanner', 'πŸ”'); this.elements = []; } buildContent() { const header = createElement('div', ['udt-panel-header']); header.innerHTML = `

${this.name}

Found: 0
`; const body = createElement('div', ['udt-panel-body', 'no-padding']); body.innerHTML = `
TagText / NameSelectorActions
`; this.el.append(header, body); } bindEvents() { qs('#udt-rescan-btn', this.el).addEventListener('click', () => this.update()); const tableBody = qs('#udt-elements-table tbody', this.el); tableBody.addEventListener('mouseover', e => { const row = e.target.closest('tr'); if (!row) return; const element = this.elements[row.dataset.index]; if (!element) return; this.toolkit.lastHighlightedElement = element; this.toolkit.updateHighlighter(); this.toolkit.dom.highlighter.style.display = 'block'; }); tableBody.addEventListener('mouseout', () => this.toolkit.removeHighlight()); tableBody.addEventListener('click', async e => { const row = e.target.closest('tr'); if (!row) return; const element = this.elements[row.dataset.index]; if(!element) return; if (e.target.closest('.copy')) { GM_setClipboard(getCssPath(element)); this.toolkit.showToast('Selector Copied!'); } if (e.target.closest('.hide')) { const selectorToHide = getCssPath(element); let hiddenSelectors = await this.toolkit.getHiddenSelectors(); if (!hiddenSelectors.includes(selectorToHide)) { hiddenSelectors.push(selectorToHide); await this.toolkit.setHiddenSelectors(hiddenSelectors); this.toolkit.showToast('Element hidden. Rescan to update.'); row.remove(); } } if (e.target.closest('.inspect')) { const inspector = this.toolkit.components.inspector; if(inspector) { inspector.update(element); this.toolkit.setActiveTab('inspector'); } } }); } async update() { this.toolkit.showToast('Scanning elements...'); const hiddenSelectors = await this.toolkit.getHiddenSelectors(); const selector = 'button, a[href], input:not([type="hidden"]), select, textarea, [role="button"], [role="link"], [tabindex]:not([tabindex="-1"])'; this.elements = Array.from(document.querySelectorAll(selector)).filter(el => { if (el.closest('[data-udt-element]')) return false; for (const hidden of hiddenSelectors) { try { if (el.matches(hidden)) return false; } catch (e) {} } return true; }); const tableBody = qs('#udt-elements-table tbody', this.el); clearElement(tableBody); this.elements.forEach((el, index) => { const text = el.textContent?.trim() || el.name || el.value || el.ariaLabel || 'N/A'; const row = createElement('tr', [], { 'data-index': index }); row.innerHTML = ` <${el.tagName.toLowerCase()}> ${text.substring(0, 50)} ${getCssPath(el).substring(0,50)}... ${ICONS.inspect} ${ICONS.copy} ${ICONS.hide} `; tableBody.append(row); }); qs('#udt-element-count', this.el).textContent = `Found: ${this.elements.length}`; } } class ResourceViewerComponent extends ToolkitComponent { constructor(toolkit) { super(toolkit, 'resource-viewer', 'Resources', '⚑'); } buildContent() { const header = createElement('div', ['udt-panel-header']); header.innerHTML = `

${this.name}

Loaded: 0
`; const body = createElement('div', ['udt-panel-body', 'no-padding']); body.innerHTML = `
NameTypeSize (KB)Time (ms)
`; this.el.append(header, body); } update() { const resources = performance.getEntriesByType("resource"); const tableBody = qs('#udt-resources-table tbody', this.el); clearElement(tableBody); resources.forEach(res => { const fileName = res.name.split('/').pop().split('?')[0]; const row = createElement('tr'); row.innerHTML = ` ${fileName.substring(0, 50)}... ${res.initiatorType} ${(res.transferSize / 1024).toFixed(2)} ${res.duration.toFixed(2)}`; tableBody.append(row); }); qs('#udt-resource-count', this.el).textContent = `Loaded: ${resources.length}`; } } class FormsComponent extends ToolkitComponent { constructor(toolkit) { super(toolkit, 'forms-analyzer', 'Forms', 'πŸ“'); } buildContent() { const header = createElement('div', ['udt-panel-header']); header.innerHTML = `

Forms Analyzer

`; const body = createElement('div', ['udt-panel-body']); this.el.append(header, body); } update() { const body = qs('.udt-panel-body', this.el); clearElement(body); const forms = qsa('form'); if (forms.length === 0) { body.textContent = 'No forms found on this page.'; return; } forms.forEach((form, index) => { const formContainer = createElement('div', ['udt-settings-group']); const formTitle = createElement('h3', [], {}, `Form #${index + 1} (Action: ${form.action.substring(0, 50)}...)`); formContainer.append(formTitle); const table = createElement('table', ['udt-table']); table.innerHTML = `NameTypeValue`; const tbody = createElement('tbody'); const inputs = qsa('input, select, textarea', form); inputs.forEach(input => { const row = createElement('tr'); row.innerHTML = `${input.name || '(no name)'}${input.type}${input.value.substring(0, 100)}`; tbody.appendChild(row); }); table.appendChild(tbody); formContainer.appendChild(table); body.appendChild(formContainer); }); } } class StorageComponent extends ToolkitComponent { constructor(toolkit) { super(toolkit, 'storage-inspector', 'Storage', 'πŸ’Ύ'); } buildContent() { const header = createElement('div', ['udt-panel-header']); header.innerHTML = `

Storage Inspector

`; const body = createElement('div', ['udt-panel-body']); this.el.append(header, body); } update() { const body = qs('.udt-panel-body', this.el); clearElement(body); this.renderStorageSection('Cookies', this.parseCookies(), body); this.renderStorageSection('Local Storage', Object.entries(localStorage), body); this.renderStorageSection('Session Storage', Object.entries(sessionStorage), body); } parseCookies() { return document.cookie.split(';').map(cookie => { const parts = cookie.trim().split('='); return [parts[0], parts.slice(1).join('=')]; }); } renderStorageSection(title, data, parent) { const container = createElement('div', ['udt-settings-group']); container.append(createElement('h3', [], {}, `${title} (${data.length})`)); if (data.length === 0 || (data.length === 1 && !data[0][0])) { container.append(createElement('p', [], {style:'opacity:0.7'}, `No ${title.toLowerCase()} found.`)); parent.append(container); return; } const table = createElement('table', ['udt-table']); table.innerHTML = `KeyValue`; const tbody = createElement('tbody'); data.forEach(([key, value]) => { const row = createElement('tr'); row.innerHTML = `${key.substring(0, 50)}${value.substring(0, 100)}`; tbody.appendChild(row); }); table.appendChild(tbody); container.appendChild(table); parent.appendChild(container); } } class AIComponent extends ToolkitComponent { constructor(toolkit) { super(toolkit, 'ai', 'AI', '🧠'); this.context = { data: '', userScript: '' }; } buildContent() { const header = createElement('div', ['udt-panel-header']); header.append(createElement('h2', ['udt-panel-title'], {}, 'Smart Prompt Aggregator')); const body = createElement('div', ['udt-panel-body']); const btnContainer = createElement('div', [], { style: 'display:flex; gap:10px; margin-top:10px;' }); btnContainer.append( createElement('button', ['udt-btn', 'primary'], {id: 'ai-send-btn'}, 'Send to AI'), createElement('button', ['udt-btn', 'secondary'], {id: 'ai-copy-btn'}, 'Copy Full Prompt') ); body.append( createElement('select', ['udt-select'], { id: 'ai-prompt-template', style: 'margin-bottom: 10px;' }), createElement('textarea', ['udt-textarea'], {id: 'ai-prompt-preview', rows: 8, placeholder: 'Click a 🧠 button or select a template to begin...'}), btnContainer, createElement('h5', [], {style: 'margin-top: 15px;'}, 'Upload Userscript for Context'), createElement('input', ['udt-input'], {type: 'file', id: 'ai-upload-script', accept: '.js,.user.js'}), createElement('span', [], {id: 'ai-upload-filename', style: 'margin-left: 10px; opacity: 0.7;'}), createElement('h5', [], {style: 'margin-top: 15px;'}, 'AI Response'), createElement('div', ['ai-response-box', 'udt-textarea'], {style: 'min-height: 100px;'}, 'AI response will appear here...') ); this.el.append(header, body); } render() { super.render(); this.loadPromptTemplates(); return this.el; } loadPromptTemplates() { const templates = this.toolkit.settings.get('ai.promptTemplates'); const select = qs('#ai-prompt-template', this.el); clearElement(select); for (const key in templates) { select.add(new Option(templates[key].name, key)); } } setPromptContext(data, templateKey) { this.context.data = data; qs('#ai-prompt-template', this.el).value = templateKey; this.compilePrompt(); this.toolkit.setActiveTab(this.id); } compilePrompt() { const templates = this.toolkit.settings.get('ai.promptTemplates'); const selectedKey = qs('#ai-prompt-template', this.el).value; const template = templates[selectedKey].prompt; let fullPrompt = `${template}\n\n--- CONTEXT DATA ---\n${this.context.data}`; if (this.context.userScript) { fullPrompt += `\n\n--- FULL USERSCRIPT FOR ANALYSIS ---\n${this.context.userScript}`; } qs('#ai-prompt-preview', this.el).value = fullPrompt; } sendToAI() { const aiPrefs = this.toolkit.settings.get('ai'); const prompt = qs('#ai-prompt-preview', this.el).value; if (!prompt) { this.toolkit.showToast('Prompt is empty.'); return; } if (aiPrefs.mode === 'newtab') { const chatUrl = { gemini: 'https://gemini.google.com/app', chatgpt: 'https://chat.openai.com/', claude: 'https://claude.ai/chats' }[aiPrefs.provider] || 'https://google.com/search?q='; GM_setClipboard(prompt); window.open(chatUrl, '_blank'); this.toolkit.showToast('Prompt copied! Paste it into the new tab.'); } else { this.toolkit.showToast('API mode is not yet implemented.'); } } bindEvents() { qs('#ai-prompt-template', this.el).addEventListener('change', () => this.compilePrompt()); qs('#ai-send-btn', this.el).addEventListener('click', () => this.sendToAI()); qs('#ai-copy-btn', this.el).addEventListener('click', () => { GM_setClipboard(qs('#ai-prompt-preview', this.el).value); this.toolkit.showToast('Full prompt copied!'); }); qs('#ai-upload-script', this.el).addEventListener('change', e => { const file = e.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (evt) => { this.context.userScript = evt.target.result; qs('#ai-upload-filename', this.el).textContent = file.name; this.compilePrompt(); }; reader.readAsText(file); } }); } } class InspectorComponent extends ToolkitComponent { constructor(toolkit) { super(toolkit, 'inspector', 'Inspector', 'DOM'); this.selectedElement = null; } buildContent() { const header = createElement('div', ['udt-panel-header']); header.innerHTML = `

${this.name}

`; const body = createElement('div', ['udt-panel-body']); body.innerHTML = `
`; this.el.append(header, body); } bindEvents() { qs('.picker-btn', this.el).addEventListener('click', () => { if (this.toolkit.isPickerActive) { this.toolkit.stopPicker(); } else { if (this.selectedElement) this.selectedElement.classList.remove('element-selected-permanent'); this.toolkit.startPicker(target => this.update(target), 'crosshair', true, this); } }); const navClickListener = e => { const targetCrumb = e.target.closest('.dom-navigator-item'); if(targetCrumb && targetCrumb.elementRef) this.update(targetCrumb.elementRef); }; qs('#inspector-breadcrumbs', this.el).addEventListener('click', navClickListener); qs('#inspector-children-list', this.el).addEventListener('click', navClickListener); } update(target) { if (this.selectedElement) this.selectedElement.classList.remove('element-selected-permanent'); this.selectedElement = target; this.selectedElement.classList.add('element-selected-permanent'); this.renderBreadcrumbs(); this.renderChildrenList(); this.renderResults(); const stripperComp = this.toolkit.components['html-stripper']; if(stripperComp && stripperComp.el) { const rawInput = qs('#stripper-raw-html', stripperComp.el); if (rawInput) { rawInput.value = this.selectedElement.outerHTML; rawInput.dispatchEvent(new Event('input', { bubbles: true })); } } } createNavElement(node) { let desc = node.tagName.toLowerCase(); if (node.id) desc += `#${node.id.split(' ')[0]}`; if (node.className && typeof node.className === 'string') { const cls = node.className.trim().split(' ')[0]; if(cls) desc += `.${cls}`; } const crumb = createElement('div', ['udt-input'], {style: 'cursor:pointer; margin-bottom:5px;'}); crumb.textContent = desc; crumb.elementRef = node; crumb.classList.add('dom-navigator-item'); return crumb; } renderBreadcrumbs() { const container = qs('#inspector-breadcrumbs', this.el); clearElement(container); container.append(createElement('h4', [], {}, 'Parents')); let path = []; let el = this.selectedElement; while (el && el.tagName !== 'BODY') { path.unshift(el); el = el.parentElement; } path.forEach(node => { const crumb = this.createNavElement(node); if (node === this.selectedElement) crumb.style.borderColor = 'var(--accent-primary)'; container.append(crumb); }); } renderChildrenList() { const container = qs('#inspector-children-list', this.el); clearElement(container); if (this.selectedElement && this.selectedElement.children.length > 0) { container.append(createElement('h4', [], {}, 'Children')); Array.from(this.selectedElement.children).forEach(node => container.append(this.createNavElement(node))); } } renderResults() { const container = qs('#inspector-results', this.el); clearElement(container); container.append(this.createInputGroup('CSS Path:', 'css-path-result', getCssPath(this.selectedElement))); const outerHTMLTextarea = this.createInputGroup('OuterHTML:', 'outerhtml-result', this.selectedElement.outerHTML); qs('input', outerHTMLTextarea).replaceWith(createElement('textarea', ['udt-textarea'], {id: 'outerhtml-result', readonly:true, rows: 6, value:this.selectedElement.outerHTML})); container.append(outerHTMLTextarea); } } class HtmlStripperComponent extends ToolkitComponent { constructor(toolkit) { super(toolkit, 'html-stripper', 'Stripper', 'βœ‚οΈ'); this.ATTRIBUTE_WHITELIST = [ 'id', 'class', 'role', 'href', 'src', 'alt', 'title', 'aria-label', 'aria-labelledby', 'aria-describedby', 'tabindex', 'target', 'rel', 'd' ]; } buildContent() { const header = createElement('div', ['udt-panel-header']); header.append(createElement('h2', ['udt-panel-title'], {}, this.name)); const body = createElement('div', ['udt-panel-body', 'stripper-container']); const ioGrid = createElement('div', [], {style:'display:grid; grid-template-columns: 1fr 1fr; gap:15px; height:100%;'}); const inputCol = createElement('div', [], {style: 'display:flex; flex-direction:column;'}); inputCol.append( createElement('h4', [], { for: 'stripper-raw-html' }, 'Raw HTML'), createElement('textarea', ['udt-textarea'], { id: 'stripper-raw-html', placeholder: 'Paste outerHTML here...', style:'flex-grow:1;' }) ); const outputCol = createElement('div', [], {style: 'display:flex; flex-direction:column; position: relative;'}); const copyBtn = createElement('button', ['udt-btn', 'secondary', 'udt-copy-btn'], {title: 'Copy', style: 'position: absolute; top: 0; right: 0;'}); copyBtn.innerHTML = ICONS.copy; outputCol.append( createElement('h4', [], { for: 'stripper-clean-html' }, 'Condensed HTML'), copyBtn, createElement('textarea', ['udt-textarea'], { id: 'stripper-clean-html', readonly: true, placeholder: 'Clean HTML appears here...', style:'flex-grow:1; margin-top: 8px;' }) ); ioGrid.append(inputCol, outputCol); body.append(ioGrid); this.el.append(header, body); } bindEvents() { qs('#stripper-raw-html', this.el).addEventListener('input', () => this.stripAndDisplay()); qs('.udt-copy-btn', this.el).addEventListener('click', () => { const output = qs('#stripper-clean-html', this.el); if (output.value) { GM_setClipboard(output.value); this.toolkit.showToast('Condensed HTML Copied!'); } }); } stripAndDisplay() { const rawHtmlInput = qs('#stripper-raw-html', this.el); const strippedHtmlOutput = qs('#stripper-clean-html', this.el); if (!rawHtmlInput.value) { strippedHtmlOutput.value = "Paste HTML to begin."; return; } try { const tempDiv = document.createElement('div'); tempDiv.innerHTML = rawHtmlInput.value; this._cleanElement(tempDiv.firstElementChild); strippedHtmlOutput.value = this._condenseHtml(tempDiv.innerHTML); } catch (error) { strippedHtmlOutput.value = `Error: ${error.message}`; } } _cleanElement(element) { if (!element) return; Array.from(element.attributes).forEach(attr => { if (!this.ATTRIBUTE_WHITELIST.includes(attr.name.toLowerCase())) { element.removeAttribute(attr.name); } }); Array.from(element.children).forEach(child => this._cleanElement(child)); } _condenseHtml(htmlString) { return htmlString.replace(/>\s+<').trim(); } } class uBlockComponent extends ToolkitComponent { constructor(toolkit) { super(toolkit, 'ublock', 'uBlock', 'πŸ›‘οΈ'); this.focusedElement = null; } buildContent() { const header = createElement('div', ['udt-panel-header']); header.innerHTML = `

uBlock Filter Generator

`; const body = createElement('div', ['udt-panel-body']); body.innerHTML = `

`; this.el.append(header, body); } bindEvents() { qs('.picker-btn', this.el).addEventListener('click', () => { if (this.toolkit.isPickerActive) this.toolkit.stopPicker(); else this.toolkit.startPicker(target => this.update(target), 'crosshair', true, this); }); qs('#ublock-navigator', this.el).addEventListener('click', e => { const targetItem = e.target.closest('.dom-navigator-item'); if (targetItem?.elementRef) this.update(targetItem.elementRef); }); } update(target) { if (this.focusedElement) this.focusedElement.classList.remove('element-selected-permanent'); this.focusedElement = target; this.focusedElement.classList.add('element-selected-permanent'); this.renderNavigator(); this.generateAndShowFilters(); } createNavItem(el, label) { if (!el || el === document.documentElement) return null; let desc = el.tagName.toLowerCase(); if (el.id) desc += `#${el.id.split(' ')[0]}`; if (el.className && typeof el.className === 'string') { const cls = el.className.trim().split(' ')[0]; if(cls) desc += `.${cls}`; } const item = createElement('div', ['dom-navigator-item', 'udt-input'], {style: 'cursor:pointer; margin-bottom:5px;'}); item.innerHTML = `${label}${desc}`; item.elementRef = el; if (el === this.focusedElement) item.style.borderColor = 'var(--accent-primary)'; return item; } renderNavigator() { const container = qs('#ublock-navigator', this.el); clearElement(container); container.append(this.createNavItem(this.focusedElement.parentElement, 'Parent'), this.createNavItem(this.focusedElement, 'Target')); if (this.focusedElement.children.length > 0) { const childrenLabel = createElement('h4', [], {style:'margin: 10px 0 5px;'}, 'Children:'); container.append(childrenLabel); Array.from(this.focusedElement.children).slice(0, 5).forEach(child => container.append(this.createNavItem(child, '↳'))); } } generateAndShowFilters() { const output = qs('#ublock-output', this.el); clearElement(output); const filters = this.generateFilters(this.focusedElement); if (filters.length === 0) { output.textContent = 'Could not generate specific filters.'; return; } filters.forEach(f => output.append(this.createInputGroup(f.desc, `ublock-${f.desc.replace(/\s/g, '-')}`, f.rule))); } generateFilters(el) { const filters = new Map(); const domain = window.location.hostname; const tag = el.tagName.toLowerCase(); const add = (desc, rule) => { if (rule && !filters.has(rule)) filters.set(rule, { desc, rule }); }; const escapeForRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); if (el.id) add('By ID (Strongest)', `${domain}##${tag}#${el.id}`); const cleanClasses = Array.from(el.classList).filter(c => !c.startsWith('element-') && !/^[a-zA-Z0-9_-]{20,}/.test(c)); if (cleanClasses.length > 0) add('By Classes', `${domain}##${tag}.${cleanClasses.join('.')}`); ['data-testid', 'aria-label', 'name', 'title'].forEach(attr => { if (el.hasAttribute(attr)) add(`By [${attr}]`, `${domain}##${tag}[${attr}="${el.getAttribute(attr)}"]`); }); const directText = Array.from(el.childNodes).find(n => n.nodeType === Node.TEXT_NODE && n.textContent.trim().length > 3)?.textContent.trim(); if (directText) add('By Text Content', `${domain}##${tag}:has-text(/${escapeForRegex(directText)}/)`); const parent = el.parentElement; if (parent) { const parentTag = parent.tagName.toLowerCase(); if (parent.id) add('By Parent ID', `${domain}##${parentTag}#${parent.id} > ${tag}`); const parentCleanClasses = Array.from(parent.classList).filter(c => !c.startsWith('element-') && !/^[a-zA-Z0-9_-]{20,}/.test(c)); if (parentCleanClasses.length > 0) add('By Parent Class', `${domain}##${parentTag}.${parentCleanClasses.join('.')} > ${tag}`); const childIndex = Array.from(parent.children).indexOf(el) + 1; add('By Child Index', `${domain}##${parentTag} > ${tag}:nth-child(${childIndex})`); } return Array.from(filters.values()); } } class CSSViewerComponent extends ToolkitComponent { constructor(toolkit) { super(toolkit, 'css-viewer', 'Styles', '🎨'); this.rawCss = ''; } buildContent() { const header = createElement('div', ['udt-panel-header']); header.innerHTML = `

Page Stylesheet Viewer

`; const body = createElement('div', ['udt-panel-body']); const btnContainer = createElement('div', [], { style: 'display:flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px;' }); btnContainer.innerHTML = ` `; const toggleContainer = createElement('div', ['udt-settings-row'], {style: 'font-size: 0.9em;'}); toggleContainer.append( createElement('label', [], {}, 'Remove junk/empty styles'), this.toolkit.createToggle('clean-css-toggle', true) ); const logBox = createElement('pre', ['udt-textarea'], { id: 'styles-log-box', 'data-placeholder': "Click 'Fetch' to load styles.", style: 'max-height: none; flex-grow: 1; min-height: 400px;' }); body.append(btnContainer, toggleContainer, createElement('hr', [], { style: 'border-color: var(--border-primary); margin: 15px 0;' }), logBox); this.el.append(header, body); } bindEvents() { qs('#fetch-styles-btn', this.el).addEventListener('click', () => this.fetchAllStyles()); qs('#export-styles-btn', this.el).addEventListener('click', () => this.exportCss()); qs('#copy-styles-btn', this.el).addEventListener('click', () => { GM_setClipboard(qs('#styles-log-box', this.el).textContent); this.toolkit.showToast('CSS copied!'); }); qs('#clean-css-toggle', this.el).addEventListener('change', () => this.displayCss()); } exportCss() { const css = qs('#styles-log-box', this.el).textContent; if (!css) { this.toolkit.showToast('No CSS to export.'); return; } GM_download({ url: 'data:text/css;charset=utf-8,' + encodeURIComponent(css), name: `page-styles-${window.location.hostname}.css`}); } async fetchAllStyles() { qs('#styles-log-box', this.el).textContent = 'Fetching...'; this.toolkit.showToast('Starting stylesheet fetch...'); const promises = Array.from(document.styleSheets).map(sheet => { try { if (sheet.href) { return new Promise(resolve => { GM_xmlhttpRequest({ method: 'GET', url: sheet.href, onload: r => resolve(r.responseText), onerror: () => resolve(`/* FAILED to fetch ${sheet.href} (CSP issue?) */`) }); }); } else if (sheet.cssRules) { return Promise.resolve(Array.from(sheet.cssRules).map(r => r.cssText).join('\n')); } return Promise.resolve(''); } catch (e) { return Promise.resolve(''); } }); this.rawCss = (await Promise.all(promises)).join('\n'); this.displayCss(); qs('#copy-styles-btn', this.el).style.display = 'inline-block'; qs('#export-styles-btn', this.el).style.display = 'inline-block'; this.toolkit.showToast('Style fetching complete.'); } cleanCss(css) { return css.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^[ \t]*[^{}]+\{\s*\}/gm, '').replace(/(\r\n|\n|\r){2,}/g, '$1').trim(); } displayCss() { const logBox = qs('#styles-log-box', this.el); if(logBox) logBox.textContent = qs('#clean-css-toggle', this.el).checked ? this.cleanCss(this.rawCss) : this.rawCss; } } class CSSToolsComponent extends ToolkitComponent { constructor(toolkit) { super(toolkit, 'css-tools', 'Tools', 'πŸ› οΈ'); } buildContent() { const header = createElement('div', ['udt-panel-header']); header.innerHTML = `

CSS Tools

`; const body = createElement('div', ['udt-panel-body']); body.innerHTML = `

Advanced Color Picker

Use the browser's eyedropper to select any color on the screen.

Click to Pick
`; const values = qs('#color-values', body); values.append( this.createInputGroup('HEX', 'color-hex', ''), this.createInputGroup('RGB', 'color-rgb', ''), this.createInputGroup('HSL', 'color-hsl', '') ); this.el.append(header, body); } bindEvents() { qs('#color-preview', this.el).addEventListener('click', () => this.startColorPicker()); } async startColorPicker() { if (!('EyeDropper' in window)) { this.toolkit.showToast('EyeDropper not supported. Picking element background instead.'); this.toolkit.startPicker(target => this.updateColorDisplay(window.getComputedStyle(target).backgroundColor), 'copy', false, this); return; } try { const result = await new EyeDropper().open(); this.updateColorDisplay(result?.sRGBHex); } catch (e) { this.toolkit.showToast('Color picking cancelled.'); } } updateColorDisplay(color) { if (!color) return; const hexColor = color.startsWith('rgb') ? `#${[...color.matchAll(/\d+/g)].map(m=>parseInt(m[0]).toString(16).padStart(2,'0')).join('')}`.toUpperCase() : color.toUpperCase(); qs('#color-preview', this.el).style.backgroundColor = hexColor; qs('#color-hex', this.el).value = hexColor; qs('#color-rgb', this.el).value = colorUtils.hexToRgb(hexColor); qs('#color-hsl', this.el).value = colorUtils.hexToHsl(hexColor); } } class DebuggerComponent extends ToolkitComponent { constructor(toolkit) { super(toolkit, 'debugger', 'Pause', '⏸️'); } buildContent() { const header = createElement('div', ['udt-panel-header']); header.innerHTML = `

JS Debugger

`; const body = createElement('div', ['udt-panel-body']); const container = createElement('div', ['udt-settings-row'], { style: 'margin-bottom: 15px;' }); const select = createElement('select', ['udt-select'], { id: 'debugger-delay' }); [['Instant', 0], ['3s', 3000], ['6s', 6000], ['9s', 9000]].forEach(([text, val]) => select.add(new Option(text, val))); container.append( createElement('label', [], { for: 'debugger-delay' }, 'Pause Delay:'), select, createElement('button', ['udt-btn', 'primary'], { id: 'trigger-debugger-btn' }, 'Trigger Pause') ); body.append( container, createElement('hr'), createElement('h3', [], {style:'margin-top:15px;'}, 'Userscript Error Log'), createElement('div', ['udt-textarea'], {id: 'userscript-error-log'}, 'No userscript errors captured.') ); this.el.append(header, body); } bindEvents() { qs('#trigger-debugger-btn', this.el).addEventListener('click', () => { const isOpen = window.outerWidth - window.innerWidth > 160 || window.outerHeight - window.innerHeight > 160; if (!isOpen) { alert('DevTools must be open (F12) to use the debugger.'); return; } const delay = parseInt(qs('#debugger-delay', this.el).value, 10); if (delay === 0) { this.toolkit.showToast('Pausing now...'); debugger; return; } let remaining = delay / 1000; const countdown = setInterval(() => { this.toolkit.showToast(`Pausing in ${remaining--}s...`, 900); if (remaining < 0) { clearInterval(countdown); debugger; } }, 1000); }); window.addEventListener('error', (e) => this.handleGlobalError(e)); } handleGlobalError(error) { if (!error.filename || !(error.filename.includes('userscript.html') || error.filename.startsWith('blob:'))) return; const logBox = qs('#userscript-error-log', this.el); if (!logBox) return; if (logBox.textContent === 'No userscript errors captured.') clearElement(logBox); const msg = `[${new Date().toLocaleTimeString()}] ${error.message} (at ${error.filename.split('/').pop()}:${error.lineno})`; logBox.prepend(createElement('div', ['error-item'], {}, msg)); } } class SettingsManager { constructor(toolkit) { this.toolkit = toolkit; this.prefs = {}; this.STORAGE_KEY = 'udt-prefs-v7.0'; } getDefaults() { const defaultOrder = this.toolkit.componentDefinitions.map(c => new c(this.toolkit).id); return { ui: { isDarkMode: true, positionSide: 'left', isPinned: false }, activeTab: 'inspector', components: Object.fromEntries(this.toolkit.componentDefinitions.map(c => [new c(this.toolkit).id, { enabled: true, showInToolbar: true }])), ai: { enabled: true, provider: 'gemini', mode: 'newtab', promptTemplates: { errorFix: { name: 'Fix an Error', prompt: 'You are a professional userscript JavaScript developer specializing in debugging. Analyze the following captured error message and suggest a fix. If a full script is provided, reference it to provide a more accurate solution.' }, domExplain: { name: 'Explain an Element', prompt: 'You are a web developer expert. Describe the following HTML element, its purpose, and any notable attributes.' } }}, 'components.order': defaultOrder }; } async load() { const defaults = this.getDefaults(); const loadedPrefs = await GM_getValue(this.STORAGE_KEY, defaults); this.prefs = loadedPrefs; for (const key in defaults) { if (typeof defaults[key] === 'object' && defaults[key] !== null && !Array.isArray(defaults[key])) { this.prefs[key] = { ...defaults[key], ...(this.prefs[key] || {}) }; } else if (this.prefs[key] === undefined) { this.prefs[key] = defaults[key]; } } const componentIds = this.toolkit.componentDefinitions.map(c => new c(this.toolkit).id); if (!this.prefs['components.order'] || this.prefs['components.gorder']?.length !== componentIds.length) this.prefs['components.order'] = componentIds; Object.keys(this.prefs.components).forEach(id => { if(!componentIds.includes(id)) delete this.prefs.components[id]; }); await this.save(); } async save() { await GM_setValue(this.STORAGE_KEY, this.prefs); } get(key) { return key.split('.').reduce((o, i) => o?.[i], this.prefs); } set(key, value) { const keys = key.split('.'); let obj = this.prefs; for (let i = 0; i < keys.length - 1; i++) { obj = obj[keys[i]] = obj[keys[i]] || {}; } obj[keys[keys.length - 1]] = value; this.save(); } getComponentState(id) { return this.prefs.components[id]; } setComponentState(id, state) { this.prefs.components[id] = state; this.save(); } } function getCssPath(el) { if (!(el instanceof Element)) return ''; let path = []; while (el && el.nodeType === Node.ELEMENT_NODE) { let selector = el.nodeName.toLowerCase(); if (el.id) { selector += '#' + el.id.trim().split(' ')[0].replace(/(:|\.|\[|\]|,|=)/g, '\\$1'); path.unshift(selector); break; } else { let sib = el, nth = 1; while ((sib = sib.previousElementSibling)) { if (sib.nodeName.toLowerCase() == selector) nth++; } if (nth != 1) selector += `:nth-of-type(${nth})`; } path.unshift(selector); el = el.parentNode; } return path.join(" > "); } // --- INITIALIZATION --- new DevToolkit().init(); })();