QuickCSS [Pre-Alpha]

Userscript recreation of the fan-favorite QuickCSS. Modify CSS on any website on a per-URL (or domain/wildcard) basis.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==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 ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":"&#39;"}[m]); });
    }
    function escapeHtmlAttr(str) {
        return escapeHtml(str).replace(/\n/g, '&#10;'); // 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?)

})();