您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Userscript recreation of the fan-favorite QuickCSS. Modify CSS on any website on a per-URL (or domain/wildcard) basis.
// ==UserScript== // @name QuickCSS [Pre-Alpha] // @namespace http://tampermonkey.net/ // @version 0.0.1.1 // @description Userscript recreation of the fan-favorite QuickCSS. Modify CSS on any website on a per-URL (or domain/wildcard) basis. // @author Setnour6 // @match *://*/* // @grant GM_registerMenuCommand // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @license GPLv3 // ==/UserScript== (function() { 'use strict'; /*** Data Model ***/ let config = GM_getValue('quickCssConfig', null); if (!config || typeof config !== 'object' || !Array.isArray(config.rules)) { config = { rules: [] }; GM_setValue('quickCssConfig', config); } if (!config.shortcuts || typeof config.shortcuts !== 'object') { config.shortcuts = { enabled: true, showHints: true }; GM_setValue('quickCssConfig', config); } /*** Utility: URL Matcher ***/ function matchPattern(url, pattern, scope) { if (!pattern) return false; try { let u = new URL(url); let host = u.host; // includes port if present if (scope === 'exact' && url === pattern) return true; if (scope === 'exactDomain' && host === pattern) return true; if (scope === 'subdomain' && (host === pattern || host.endsWith('.' + pattern))) return true; if (scope === 'wildcard') { // transform pattern with * to regex (applies to whole URL). escape everything but '*' then replace '*' with '.*' let parts = pattern.split('*').map(escapeRE); let regex = new RegExp('^' + parts.join('.*') + '$'); return regex.test(url); } } catch (e) { console.warn('QuickCSS: matchPattern error', e); } return false; } function escapeRE(s){ return s.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'); } /*** Inject matched CSS ***/ function removeInjectedStyles(root) { try { const styles = root.querySelectorAll && root.querySelectorAll('style[data-quick-css]'); if (styles && styles.length) { styles.forEach(s => s.remove()); } } catch (e) { // ignore } } function injectCSS(css, root) { try { const docForCreate = (root && root.ownerDocument) ? root.ownerDocument : root; if (!docForCreate || typeof docForCreate.createElement !== 'function') return; try { removeInjectedStyles(root); } catch (e) {} const style = docForCreate.createElement('style'); style.setAttribute('data-quick-css', 'true'); style.textContent = css; if (root instanceof ShadowRoot) { root.appendChild(style); } else if (root.head) { root.head.appendChild(style); } else { (docForCreate.documentElement || docForCreate).appendChild(style); } } catch (err) { console.error('QuickCSS: Failed to inject CSS:', err); } } function walkAndInjectShadowRoots(root, cssText) { try { const walkerRoot = root.body || root; const walker = (walkerRoot && walkerRoot.ownerDocument) ? walkerRoot.ownerDocument.createTreeWalker(walkerRoot, NodeFilter.SHOW_ELEMENT, null, false) : document.createTreeWalker(walkerRoot, NodeFilter.SHOW_ELEMENT, null, false); while (walker.nextNode()) { const el = walker.currentNode; if (el && el.shadowRoot) { injectCSS(cssText, el.shadowRoot); walkAndInjectShadowRoots(el.shadowRoot, cssText); } } } catch (e) { // fallback silent } } function injectIntoDocument(doc, cssText) { try { injectCSS(cssText, doc); walkAndInjectShadowRoots(doc, cssText); } catch (err) { console.error('QuickCSS: injectIntoDocument error', err); } } function applyCustomCSS() { const url = location.href; const cssText = (config.rules || []) .filter(rule => matchPattern(url, rule.pattern, rule.scope)) .map(rule => `/* rule ${rule.id} (${rule.pattern} [${rule.scope}]) */\n${rule.css}`) .join('\n'); try { removeInjectedStyles(document); } catch(e){} if (cssText) { injectIntoDocument(document, cssText); } document.querySelectorAll('iframe').forEach(iframe => handleIframeInjection(iframe, cssText)); } function handleIframeInjection(iframe, cssText) { try { if (!iframe.contentDocument || !iframe.contentWindow) { iframe.addEventListener('load', function tryInject() { iframe.removeEventListener('load', tryInject); try { injectIntoDocument(iframe.contentDocument, cssText); } catch (e) { console.warn('QuickCSS: cannot inject into iframe after load (likely cross-origin).'); } }, { once: true }); return; } injectIntoDocument(iframe.contentDocument, cssText); } catch (e) { // console.warn('QuickCSS: Skipping cross-origin iframe.'); } } /*** Live DOM watcher for future iframes & shadow roots ***/ const domObserver = new MutationObserver(mutations => { const url = location.href; const cssText = (config.rules || []) .filter(rule => matchPattern(url, rule.pattern, rule.scope)) .map(rule => `/* rule ${rule.id} (${rule.pattern} [${rule.scope}]) */\n${rule.css}`) .join('\n'); mutations.forEach(m => { m.addedNodes && m.addedNodes.forEach(node => { if (!(node instanceof Element)) return; if (node.tagName === 'IFRAME') { handleIframeInjection(node, cssText); } if (node.shadowRoot) { try { injectCSS(cssText, node.shadowRoot); walkAndInjectShadowRoots(node.shadowRoot, cssText); } catch(e){} } node.querySelectorAll && node.querySelectorAll('iframe').forEach(f => handleIframeInjection(f, cssText)); node.querySelectorAll && node.querySelectorAll('*').forEach(el => { if (el.shadowRoot) { try { injectCSS(cssText, el.shadowRoot); walkAndInjectShadowRoots(el.shadowRoot, cssText); } catch(e){} } }); }); }); }); try { domObserver.observe(document, { childList: true, subtree: true }); } catch (e) { // ignore if cannot observe } /*** UI Overlay ***/ function openEditor() { if (document.getElementById('quick-css-editor')) return; // Prevent multiple overlays let overlay = document.createElement('div'); overlay.id = 'quick-css-editor'; Object.assign(overlay.style, { position:'fixed',top:0,left:0,width:'100%',height:'100%', background:'rgba(0,0,0,0.6)',zIndex:9999999, display:'flex',alignItems:'center',justifyContent:'center' }); let modal = document.createElement('div'); Object.assign(modal.style, { width:'600px',maxHeight:'90%',overflowY:'auto', background:'#1e1e1e',borderRadius:'8px',padding:'20px', boxShadow:'0 4px 20px rgba(0,0,0,0.5)',color:'#fff', fontFamily:'Segoe UI, sans-serif' }); modal.innerHTML = ` <div style="display:flex;justify-content:space-between;align-items:center"> <h2 style="margin:0;font-size:1.3em">Quick CSS Editor</h2> <button id="qce-close" style="background:none;border:none;color:#fff;font-size:1.2em;cursor:pointer">✕</button> </div> <div id="qce-list" style="margin-top:10px"></div> <div style="margin-top:15px;display:flex;gap:10px;flex-wrap:wrap"> <button id="qce-add" style="padding:8px 12px;border:none;border-radius:4px;background:#007acc;cursor:pointer">+ Add Rule</button> <button id="qce-export" style="padding:8px 12px;border:none;border-radius:4px;background:#6f42c1;cursor:pointer">Export</button> <button id="qce-import" style="padding:8px 12px;border:none;border-radius:4px;background:#17a2b8;cursor:pointer">Import</button> </div> `; overlay.appendChild(modal); function updateShortcutHints(modal) { const hintMap = { '#qce-close': 'Alt+Shift+Q', '#qce-save': 'Alt+Shift+S', '#qce-cancel': 'Alt+Shift+C', '#qce-add': 'Alt+Shift+N', '#qce-export': 'Alt+Shift+X', '#qce-import': 'Alt+Shift+I' }; const show = !!(config && config.shortcuts && config.shortcuts.showHints); Object.keys(hintMap).forEach(sel => { const btn = (modal && modal.querySelector(sel)) || document.querySelector(sel); if (!btn) return; const existing = btn.parentNode && btn.parentNode.querySelector('.qce-shortcut-hint[data-for="' + sel + '"]'); if (existing) existing.remove(); if (show) { const span = document.createElement('span'); span.className = 'qce-shortcut-hint'; span.setAttribute('data-for', sel); span.textContent = ' (' + hintMap[sel] + ')'; span.style.cssText = 'margin-left:6px;color:#9ecbff;font-size:0.85em;font-weight:500'; try { btn.insertAdjacentElement('afterend', span); } catch (e) { btn.parentNode && btn.parentNode.appendChild(span); } } }); } const settingsContainer = document.createElement('div'); settingsContainer.style.cssText = 'margin-top:12px;padding:8px;border-top:1px solid #333;color:#ddd;font-size:0.95em;display:flex;flex-direction:column;gap:6px;'; settingsContainer.innerHTML = ` <label style="display:flex;align-items:center;gap:8px"> <input type="checkbox" id="qce-shortcuts-enabled-checkbox"> Enable keyboard shortcuts (functionality) </label> <label style="display:flex;align-items:center;gap:8px"> <input type="checkbox" id="qce-shortcuts-show-checkbox"> Show keyboard shortcut hints in the UI </label> <div style="color:#9aa; font-size:0.85em">Tip: Open editor — <strong>Alt+Shift+E</strong>. When enabled, other shortcuts: Save (<strong>Alt+Shift+S</strong>), Cancel (<strong>Alt+Shift+C/X</strong>), Close (<strong>Alt+Shift+Q</strong>).</div> `; modal.appendChild(settingsContainer); const shortcutsEnabledCheckbox = modal.querySelector('#qce-shortcuts-enabled-checkbox'); const shortcutsShowCheckbox = modal.querySelector('#qce-shortcuts-show-checkbox'); shortcutsEnabledCheckbox.checked = !!(config && config.shortcuts && config.shortcuts.enabled); shortcutsShowCheckbox.checked = !!(config && config.shortcuts && config.shortcuts.showHints); shortcutsEnabledCheckbox.addEventListener('change', () => { config.shortcuts = config.shortcuts || {}; config.shortcuts.enabled = !!shortcutsEnabledCheckbox.checked; GM_setValue('quickCssConfig', config); // no further UI changes necessary; handler reads config directly }); shortcutsShowCheckbox.addEventListener('change', () => { config.shortcuts = config.shortcuts || {}; config.shortcuts.showHints = !!shortcutsShowCheckbox.checked; GM_setValue('quickCssConfig', config); updateShortcutHints(modal); }); updateShortcutHints(modal); const _origRenderRules = renderRules; renderRules = function() { _origRenderRules(); setTimeout(()=>updateShortcutHints(modal), 0.01); }; document.body.appendChild(overlay); document.getElementById('qce-close').onclick = () => overlay.remove(); document.getElementById('qce-add').onclick = () => renderRuleForm(); const listEl = modal.querySelector('#qce-list'); function renderRules() { listEl.innerHTML = ''; (config.rules || []).forEach(rule => { let item = document.createElement('div'); Object.assign(item.style, {padding:'8px',borderBottom:'1px solid #333',display:'flex',justifyContent:'space-between',alignItems:'center'}); item.innerHTML = ` <div style="max-width:70%;word-break:break-word"> <strong>${escapeHtml(rule.pattern)}</strong> <span style="color:#aaa">[${escapeHtml(rule.scope)}]</span> <div style="color:#9a9a9a;font-size:0.9em;margin-top:6px;white-space:pre-wrap;max-height:4.5em;overflow:hidden">${escapeHtml(rule.css)}</div> </div> <div style="flex-shrink:0"> <button data-id="${rule.id}" class="qce-edit" style="margin-right:8px">Edit</button> <button data-id="${rule.id}" class="qce-del">Delete</button> </div>`; listEl.appendChild(item); }); listEl.querySelectorAll('.qce-edit').forEach(btn=>{ btn.onclick = () => renderRuleForm(btn.dataset.id); }); listEl.querySelectorAll('.qce-del').forEach(btn=>{ btn.onclick = () => { if(confirm('Delete this rule?')) { config.rules = config.rules.filter(r=>r.id!==btn.dataset.id); GM_setValue('quickCssConfig', config); renderRules(); applyCustomCSS(); } }; }); document.getElementById('qce-export').onclick = () => { const json = JSON.stringify(config, null, 2); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(json).then(() => { alert('QuickCSS: Config copied to clipboard.'); }, () => { prompt('Copy this JSON manually:', json); }); } else { prompt('Copy this JSON:', json); } }; document.getElementById('qce-import').onclick = () => { const input = prompt('Paste your exported QuickCSS JSON:'); if (!input) return; try { const parsed = JSON.parse(input); if (parsed && parsed.rules && Array.isArray(parsed.rules)) { config = parsed; GM_setValue('quickCssConfig', config); alert('QuickCSS: Config imported.'); overlay.remove(); openEditor(); applyCustomCSS(); } else { throw new Error(); } } catch { alert('QuickCSS: Invalid JSON.'); } }; } function renderRuleForm(editId) { let existing = document.getElementById('qce-form'); if (existing) existing.remove(); let rule = editId ? (config.rules.find(r=>r.id===editId) || { id: editId, pattern:'', scope:'exactDomain', css:'' }) : { id: Date.now().toString(), pattern:'', scope:'exactDomain', css:'' }; let form = document.createElement('div'); form.id = 'qce-form'; Object.assign(form.style, {marginTop:'20px',padding:'10px',background:'#2b2b2b',borderRadius:'6px'}); form.innerHTML = ` <label>URL/Domain Pattern:</label><br> <input id="qce-pattern" value="${escapeHtmlAttr(rule.pattern)}" style="width:100%;padding:6px;margin-top:4px;background:#1e1e1e;border:1px solid #444;color:#fff;"><br><br> <label>Scope:</label><br> <select id="qce-scope" style="width:100%;padding:6px;background:#1e1e1e;border:1px solid #444;color:#fff;margin-top:4px;"> <option value="exact" ${rule.scope==='exact'?'selected':''}>Exact URL</option> <option value="exactDomain" ${rule.scope==='exactDomain'?'selected':''}>Exact Domain</option> <option value="subdomain" ${rule.scope==='subdomain'?'selected':''}>Include Subdomains</option> <option value="wildcard" ${rule.scope==='wildcard'?'selected':''}>Wildcard (*)</option> </select><br><br> <label>Custom CSS:</label><br> <textarea id="qce-css" rows="6" style="width:100%;padding:6px;background:#1e1e1e;border:1px solid #444;color:#fff;">${escapeHtmlAttr(rule.css)}</textarea><br><br> <button id="qce-save" style="padding:8px 12px;border:none;border-radius:4px;background:#28a745;cursor:pointer">Save</button> <button id="qce-cancel" style="padding:8px 12px;border:none;border-radius:4px;background:#dc3545;cursor:pointer;margin-left:8px">Cancel</button> `; listEl.prepend(form); modal.querySelector('#qce-cancel').onclick = () => form.remove(); modal.querySelector('#qce-save').onclick = () => { rule.pattern = form.querySelector('#qce-pattern').value.trim(); rule.scope = form.querySelector('#qce-scope').value; rule.css = form.querySelector('#qce-css').value; let idx = config.rules.findIndex(r=>r.id===rule.id); if (idx>=0) config.rules[idx] = rule; else config.rules.push(rule); GM_setValue('quickCssConfig', config); form.remove(); renderRules(); applyCustomCSS(); }; } renderRules(); } document.removeEventListener && document.removeEventListener('keydown', window.__qce_key_handler); window.__qce_key_handler = function (e) { if (!e.altKey || !e.shiftKey) return; if (!config || !config.shortcuts || !config.shortcuts.enabled) return; // Respect the enable/disable toggle const k = (e.key || '').toLowerCase(); // Alt+Shift+E -> toggle editor (global) if (k === 'e') { e.preventDefault(); const existing = document.getElementById('quick-css-editor'); if (existing) existing.remove(); else openEditor(); return; } // If editor isn't open, other shortcuts do nothing const overlay = document.getElementById('quick-css-editor'); if (!overlay) return; // Alt+Shift+S -> Save (click the Save button if present) if (k === 's') { e.preventDefault(); const saveBtn = overlay.querySelector('#qce-save'); if (saveBtn) saveBtn.click(); return; } // Alt+Shift+C or Alt+Shift+X -> Cancel (click the Cancel button if present) if (k === 'c' || k === 'x') { e.preventDefault(); const cancelBtn = overlay.querySelector('#qce-cancel'); if (cancelBtn) cancelBtn.click(); return; } // Alt+Shift+Q -> Close overlay if (k === 'q') { e.preventDefault(); overlay.remove(); return; } // Alt+Shift+N -> Add new rule if (k === 'n') { e.preventDefault(); const addBtn = overlay.querySelector('#qce-add'); if (addBtn) addBtn.click(); return; } // Alt+Shift+X -> Export if (k === 'x' && e.altKey && e.shiftKey && overlay) { // prefer the explicit export button if present const expBtn = overlay.querySelector('#qce-export'); if (expBtn) { e.preventDefault(); expBtn.click(); } return; } // Alt+Shift+I -> Import if (k === 'i' && overlay) { const impBtn = overlay.querySelector('#qce-import'); if (impBtn) { e.preventDefault(); impBtn.click(); } return; } }; document.addEventListener('keydown', window.__qce_key_handler); /*** Register menu command ***/ try { GM_registerMenuCommand && GM_registerMenuCommand('Quick CSS Editor…', openEditor); } catch (e) { console.warn('QuickCSS: failed to register menu command', e); } function updateShortcutHints(modal) { const hintMap = { '#qce-close': 'Alt+Shift+Q', '#qce-save': 'Alt+Shift+S', '#qce-cancel': 'Alt+Shift+C', '#qce-add': 'Alt+Shift+N', '#qce-export': 'Alt+Shift+X', '#qce-import': 'Alt+Shift+I' }; const show = !!(config && config.shortcuts && config.shortcuts.showHints); Object.keys(hintMap).forEach(sel => { const btn = (modal && modal.querySelector(sel)) || document.querySelector(sel); if (!btn) return; const existing = btn.parentNode && btn.parentNode.querySelector('.qce-shortcut-hint[data-for="' + sel + '"]'); if (existing) existing.remove(); if (show) { const span = document.createElement('span'); span.className = 'qce-shortcut-hint'; span.setAttribute('data-for', sel); span.textContent = ' (' + hintMap[sel] + ')'; span.style.cssText = 'margin-left:6px;color:#9ecbff;font-size:0.85em;font-weight:500'; try { btn.insertAdjacentElement('afterend', span); } catch (e) { btn.parentNode && btn.parentNode.appendChild(span); } // works for most layouts } }); } /*** Helpers ***/ function escapeHtml(str) { if (str == null) return ''; return String(str).replace(/[&<>"']/g, function(m){ return ({'&':'&','<':'<','>':'>','"':'"',"'":"'"}[m]); }); } function escapeHtmlAttr(str) { return escapeHtml(str).replace(/\n/g, ' '); // preserve newlines in textarea by escaping attributes, but for safety } try { applyCustomCSS(); } catch (e) {} // Apply CSS right away when script runs (Hot Reload) try { window.__quickCss = { config, applyCustomCSS }; } catch (e) {} // Expose for debugging on `window` (Optional?) })();