GitHub Advanced Search

Advanced filter modal for GitHub search

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         GitHub Advanced Search 
// @namespace    https://github.com/quantavil/userscript
// @version      4.2
// @description  Advanced filter modal for GitHub search 
// @match        https://github.com/*
// @license      MIT
// @icon         https://github.githubassets.com/favicons/favicon.svg
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
    'use strict';

    /* =========================================================================
       CONSTANTS & CONFIG
       ========================================================================= */
    const CONFIG = {
        ids: {
            modal: 'gh-adv-search-modal',
            style: 'gh-adv-search-style',
            toggleBtn: 'gh-adv-toggle-btn'
        },
        selectors: {
            results: '[data-testid="results-list"]',
            resultItem: '[data-testid="results-list"] > div, .repo-list-item, .Box-row',
            resultLink: '.search-title a, a[href^="/"]'
        }
    };

    const FIELDS = [
        {
            section: 'CORE',
            items: [
                {
                    id: 'type', label: 'TYPE', type: 'select', options: [
                        { v: 'repositories', l: 'Repositories' },
                        { v: 'code', l: 'Code' },
                        { v: 'issues', l: 'Issues' },
                        { v: 'pullrequests', l: 'Pull Requests' },
                        { v: 'discussions', l: 'Discussions' },
                        { v: 'users', l: 'Users' }
                    ]
                },
                {
                    id: 'sort', label: 'SORT', type: 'select', options: [
                        { v: '', l: 'Best Match' },
                        { v: 'stars', l: 'Most Stars' },
                        { v: 'forks', l: 'Most Forks' },
                        { v: 'updated', l: 'Recently Updated' }
                    ]
                }
            ]
        },
        {
            section: 'LOGIC',
            items: [
                { id: 'and', label: 'AND', placeholder: 'rust async', type: 'text' },
                { id: 'or', label: 'OR', placeholder: 'react, vue', type: 'text' },
            ]
        },
        {
            section: 'META',
            items: [
                { id: 'user', label: 'Owner', placeholder: 'facebook', meta: 'user' },
                { id: 'repo', label: 'Repository', placeholder: 'react', meta: 'repo' },
                { id: 'lang', label: 'Language', placeholder: 'python', meta: 'language' },
                { id: 'ext', label: 'Extension', placeholder: 'md', meta: 'extension' },
                { id: 'path', label: 'Path', placeholder: 'src/', meta: 'path' },
                { id: 'stars', label: 'Stars', placeholder: '>500', meta: 'stars' },
                { id: 'forks', label: 'Forks', placeholder: '>100', meta: 'forks' },
                { id: 'size', label: 'Size (KB)', placeholder: '>1000', meta: 'size' },
                { id: 'created', label: 'Created', placeholder: '>2023-01', meta: 'created' },
                { id: 'pushed', label: 'Pushed', placeholder: '>2024-01-01', meta: 'pushed' }
            ]
        }
    ];

    /* =========================================================================
       THEME & STYLES — MIMIMAL BRUTAL
       ========================================================================= */
    function injectStyles() {
        if (document.getElementById(CONFIG.ids.style)) return;

        const css = `
            /* Light theme by default */
            :root {
                --brutal-bg: #ffffff;
                --brutal-fg: #1a1a1a;
                --brutal-accent: #e11d48;
                --brutal-border: #1a1a1a;
                --brutal-muted: #6b7280;
                --brutal-shadow: 4px 4px 0 #1a1a1a;
                --brutal-font: system-ui, -apple-system, sans-serif;
            }

            /* --- Floating Toggle Button --- */
            #${CONFIG.ids.toggleBtn} {
                position: fixed;
                top: 50%;
                right: 0;
                transform: translateY(-50%);
                width: 28px;
                height: 44px;
                background: var(--brutal-fg);
                border: none;
                border-radius: 6px 0 0 6px;
                cursor: pointer;
                z-index: 9997;
                display: flex;
                align-items: center;
                justify-content: center;
                opacity: 0.25;
                transition: opacity 0.15s, width 0.15s;
            }
            #${CONFIG.ids.toggleBtn}:hover,
            #${CONFIG.ids.toggleBtn}:focus {
                opacity: 1;
                width: 36px;
            }
            #${CONFIG.ids.toggleBtn} svg {
                width: 16px;
                height: 16px;
                fill: var(--brutal-bg);
            }
            @media (hover: none) {
                #${CONFIG.ids.toggleBtn} {
                    opacity: 0.5;
                }
            }

            /* --- Modal Panel (Right Side) --- */
            #${CONFIG.ids.modal} {
                position: fixed;
                top: 60px;
                right: 16px;
                bottom: auto;
                left: auto;
                transform: none;
                width: 420px;
                max-width: calc(100vw - 32px);
                max-height: calc(100vh - 80px);
                background: var(--brutal-bg);
                border: 2px solid var(--brutal-border);
                box-shadow: var(--brutal-shadow);
                z-index: 9999;
                display: none;
                flex-direction: column;
                font-family: var(--brutal-font);
                font-size: 13px;
            }

            #${CONFIG.ids.modal}[data-visible="true"] {
                display: flex;
                animation: brutal-slide 0.12s ease-out;
            }

            @keyframes brutal-slide {
                from { opacity: 0; transform: translateX(8px); }
                to { opacity: 1; transform: translateX(0); }
            }

            .brutal-header {
                background: var(--brutal-border);
                color: var(--brutal-bg);
                padding: 8px 12px;
                font-weight: 700;
                text-transform: uppercase;
                letter-spacing: 1px;
                display: flex;
                justify-content: space-between;
                align-items: center;
                font-size: 11px;
            }

            .brutal-close {
                cursor: pointer;
                font-size: 18px;
                line-height: 1;
                opacity: 0.7;
                font-weight: 400;
            }
            .brutal-close:hover { opacity: 1; }

            .brutal-body {
                padding: 12px;
                overflow-y: auto;
                scrollbar-width: none;
                -ms-overflow-style: none;
            }
            .brutal-body::-webkit-scrollbar { display: none; }

            .brutal-section {
                margin-bottom: 12px;
            }
            .brutal-section:last-child { margin-bottom: 0; }

            .brutal-section-title {
                font-weight: 700;
                font-size: 9px;
                text-transform: uppercase;
                letter-spacing: 1.5px;
                color: var(--brutal-muted);
                margin-bottom: 8px;
                padding-bottom: 4px;
                border-bottom: 1px solid var(--brutal-border);
            }

            .brutal-grid {
                display: grid;
                grid-template-columns: 1fr 1fr;
                gap: 8px;
            }
            .brutal-grid.full { grid-template-columns: 1fr; }

            .brutal-field label {
                display: block;
                font-size: 9px;
                font-weight: 700;
                text-transform: uppercase;
                letter-spacing: 0.5px;
                margin-bottom: 3px;
                color: var(--brutal-fg);
            }

            .brutal-input {
                width: 100%;
                background: var(--brutal-bg);
                border: 2px solid var(--brutal-border);
                padding: 6px 8px;
                color: var(--brutal-fg);
                font-family: inherit;
                font-size: 12px;
                box-sizing: border-box;
            }
            .brutal-input:focus {
                outline: none;
                border-color: var(--brutal-accent);
            }
            .brutal-input::placeholder {
                color: var(--brutal-muted);
            }

            .brutal-footer {
                padding: 10px 12px;
                border-top: 2px solid var(--brutal-border);
                display: flex;
                gap: 8px;
            }

            .brutal-btn {
                flex: 1;
                padding: 8px;
                border: 2px solid var(--brutal-border);
                background: var(--brutal-bg);
                font-family: inherit;
                font-weight: 700;
                font-size: 10px;
                text-transform: uppercase;
                letter-spacing: 0.5px;
                cursor: pointer;
                color: var(--brutal-fg);
                transition: transform 0.05s, box-shadow 0.05s;
            }
            .brutal-btn:hover {
                transform: translate(-2px, -2px);
                box-shadow: 4px 4px 0 var(--brutal-border);
            }
            .brutal-btn:active {
                transform: translate(0, 0);
                box-shadow: none;
            }
            .brutal-btn.primary {
                background: var(--brutal-border);
                color: var(--brutal-bg);
            }

            /* --- Release Badge (Brutal Enhanced) --- */
            .gh-release-tag {
                display: inline-flex;
                align-items: center;
                padding: 4px 8px;
                margin-top: 6px;
                font-size: 11px;
                font-family: var(--brutal-font);
                font-weight: 700;
                border: 2px solid var(--brutal-border);
                background: var(--brutal-bg);
                color: var(--brutal-fg) !important;
                text-decoration: none !important;
                box-shadow: 2px 2px 0 var(--brutal-border);
                transition: transform 0.1s;
            }
            .gh-release-tag:hover {
                transform: translate(-1px, -1px);
                box-shadow: 3px 3px 0 var(--brutal-border);
                text-decoration: none !important;
            }
            .gh-release-tag.loading { opacity: 0.5; cursor: wait; border-style: dashed; }
            
            /* Success (Has Release) - GREEN */
            .gh-release-tag.has-release {
                color: #15803d !important;
                border-color: #15803d;
                box-shadow: 2px 2px 0 #15803d;
            }
            .gh-release-tag.has-release:hover {
                background: #f0fdf4;
                box-shadow: 3px 3px 0 #15803d;
            }

            /* Failure (No Release) - RED */
            .gh-release-tag.no-release {
                color: #b91c1c !important;
                border-color: #b91c1c;
                box-shadow: 2px 2px 0 #b91c1c;
            }
            .gh-release-tag.no-release:hover {
                background: #fef2f2;
                box-shadow: 3px 3px 0 #b91c1c;
            }

            /* --- Checkbox --- */
            .brutal-check-row {
                display: flex;
                align-items: center;
                gap: 8px;
            }
            .brutal-check {
                width: 16px;
                height: 16px;
                accent-color: var(--brutal-accent);
                cursor: pointer;
                border: 2px solid var(--brutal-border);
            }
            .brutal-check-label {
                font-size: 11px;
                font-weight: 700;
                text-transform: uppercase;
                cursor: pointer;
            }

            /* --- Overlay --- */
            .brutal-overlay {
                position: fixed;
                inset: 0;
                background: rgba(0, 0, 0, 0.4);
                backdrop-filter: blur(2px);
                z-index: 9998;
                display: none;
            }
            .brutal-overlay[data-visible="true"] { display: block; }

            /* Mobile responsive */
            @media (max-width: 480px) {
                #${CONFIG.ids.modal} {
                    right: 0;
                    left: 0;
                    bottom: 0;
                    width: 100%;
                    max-width: none;
                    top: auto;
                    max-height: 85vh;
                    border-left: none;
                    border-right: none;
                    border-bottom: none;
                }
            }
        `;

        const style = document.createElement('style');
        style.id = CONFIG.ids.style;
        style.textContent = css;
        document.head.appendChild(style);
    }

    /* =========================================================================
       LOGIC: QUERY BUILDER 
       ========================================================================= */
    class QueryBuilder {
        static clean(str) {
            if (!str) return [];
            // Parse tokens respecting quotes: matches quoted strings OR non-space/comma sequences
            const matches = str.match(/("[^"]*"|[^, ]+)/g);
            return matches ? matches : [];
        }

        static buildUrl(data) {
            const parts = [];

            const andTerms = this.clean(data.and);
            if (andTerms.length) parts.push(...andTerms);

            const orTerms = this.clean(data.or);
            if (orTerms.length) {
                parts.push(orTerms.length === 1 ? orTerms[0] : `(${orTerms.join(' OR ')})`);
            }

            data.meta.forEach(m => {
                let val = m.value.trim();
                if (!val) return;
                // Auto-add >= for specific numeric fields if missing operator
                if (['stars', 'forks', 'size'].includes(m.key)) {
                    if (!val.match(/^[<>=]/) && !val.includes('..')) val = `>=${val}`;
                }
                parts.push(`${m.key}:${val}`);
            });

            const query = encodeURIComponent(parts.join(' '));
            let url = `https://github.com/search?q=${query}&type=${data.type}`;
            if (data.sort) url += `&s=${data.sort}&o=desc`;
            if (data.releasesOnly) url += '&userscript_has_release=1';

            return url;
        }

        static parseCurrent() {
            const params = new URLSearchParams(window.location.search);
            const rawQ = params.get('q') || '';
            const type = params.get('type') || '';
            const sort = params.get('s') || '';
            const releasesOnly = params.get('userscript_has_release') === '1';

            const state = { type, sort, releasesOnly, and: '', or: '', meta: {} };
            let q = rawQ;

            // 1. Extract Meta fields (key:value) - supports quoted values
            FIELDS.find(s => s.section === 'META').items.forEach(item => {
                const regex = new RegExp(`\\b${item.meta}:("[^"]*"|\\S+)`, 'gi');
                q = q.replace(regex, (match, val) => {
                    // Strip auto >= for display
                    if (['stars', 'forks', 'size'].includes(item.meta) && val.startsWith('>=')) {
                        val = val.substring(2);
                    }
                    state.meta[item.id] = val;
                    return ''; // Remove from q
                });
            });

            // 2. Extract OR groups: (a OR b)
            const orMatch = q.match(/\(([^)]+)\)/);
            if (orMatch && orMatch[1].includes(' OR ')) {
                state.or = orMatch[1].split(' OR ').join(', ');
                q = q.replace(orMatch[0], '');
            }

            // 3. Remaining is AND
            state.and = q.replace(/\s+/g, ' ').trim();

            return state;
        }
    }

    /* =========================================================================
       LOGIC: RELEASE DETECTION
       ========================================================================= */
    const CACHE_PREFIX = 'gh-release-cache-';
    const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours

    function getReleaseCache(owner, repo) {
        try {
            const key = `${CACHE_PREFIX}${owner}-${repo}`;
            const cached = localStorage.getItem(key);
            if (!cached) return null;

            const data = JSON.parse(cached);
            if (Date.now() - data.timestamp > CACHE_TTL) {
                localStorage.removeItem(key);
                return null;
            }
            return data.info;
        } catch (e) {
            return null;
        }
    }

    function setReleaseCache(owner, repo, info) {
        try {
            const key = `${CACHE_PREFIX}${owner}-${repo}`;
            const data = {
                timestamp: Date.now(),
                info: info
            };
            localStorage.setItem(key, JSON.stringify(data));
        } catch (e) { }
    }

    function formatRelativeDate(dateStr) {
        const date = new Date(dateStr);
        const now = new Date();
        const diffMs = now - date;
        const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));

        if (diffDays === 0) return 'today';
        if (diffDays === 1) return 'yesterday';
        if (diffDays < 7) return `${diffDays}d ago`;
        if (diffDays < 30) return `${Math.floor(diffDays / 7)}w ago`;
        if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`;
        return `${Math.floor(diffDays / 365)}y ago`;
    }

    function createReleaseBadge(status, data = null) {
        const badge = document.createElement('a');
        badge.className = `gh-release-tag`;

        if (status === 'checking') {
            badge.textContent = 'Checking...';
            badge.classList.add('loading');
            badge.href = '#';
            badge.onclick = (e) => e.preventDefault();
        } else if (status === 'has-release' && data) {
            const dateText = data.date ? ` (${formatRelativeDate(data.date)})` : '';
            badge.textContent = `${data.tag}${dateText}`;
            badge.href = data.url;
            badge.target = '_blank';
            badge.title = data.date ? `Released: ${new Date(data.date).toLocaleDateString()}` : data.tag;
            badge.classList.add('has-release');
        } else {
            badge.textContent = 'NO RELEASE';
            badge.classList.add('no-release');
            badge.href = '#';
            badge.onclick = (e) => e.preventDefault();
        }

        return badge;
    }

    async function fetchReleaseInfo(owner, repo) {
        const cached = getReleaseCache(owner, repo);
        if (cached) return cached;

        try {
            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), 10000);

            const res = await fetch(`https://github.com/${owner}/${repo}/releases/latest`, {
                method: 'GET',
                redirect: 'follow',
                signal: controller.signal
            });
            clearTimeout(timeoutId);

            if (res.status === 404 || !res.ok) {
                setReleaseCache(owner, repo, null);
                return null;
            }

            const htmlText = await res.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(htmlText, 'text/html');

            let tag = null;
            const finalUrl = res.url;
            const tagMatch = finalUrl.match(/\/releases\/tag\/([^/?#]+)/);

            if (tagMatch) {
                tag = decodeURIComponent(tagMatch[1]);
            } else {
                const title = doc.title;
                const titleMatch = title.match(/Release (.+?) ·/);
                if (titleMatch) tag = titleMatch[1];
            }

            if (!tag) {
                const header = doc.querySelector('h1.d-inline');
                if (header) tag = header.textContent.trim();
            }

            if (!tag) return null;

            let date = null;
            const timeEl = doc.querySelector('relative-time');
            if (timeEl) {
                date = timeEl.getAttribute('datetime');
            } else {
                const anyTime = doc.querySelector('time[datetime]');
                if (anyTime) date = anyTime.getAttribute('datetime');
            }

            const info = {
                tag,
                date,
                url: `https://github.com/${owner}/${repo}/releases/tag/${encodeURIComponent(tag)}`
            };

            setReleaseCache(owner, repo, info);
            return info;

        } catch (e) {
            return null;
        }
    }

    async function processQueue(items, concurrency, task) {
        const queue = [...items];
        const workers = [];

        const worker = async () => {
            while (queue.length > 0) {
                const item = queue.shift();
                try {
                    await task(item);
                } catch (e) { }
            }
        };

        for (let i = 0; i < concurrency; i++) {
            workers.push(worker());
        }

        await Promise.all(workers);
    }

    async function processItem(item, filterOnly) {
        const link = item.querySelector(CONFIG.selectors.resultLink);
        if (!link) return;

        const path = link.getAttribute('href');
        const parts = path.split('/').filter(Boolean);
        if (parts.length < 2) return;
        const [owner, repo] = parts;

        const metaList = item.querySelector('ul');
        const insertTarget = metaList || item;

        // Container is still used to keep structure clean, but no specific asset classes needed
        const badgeContainer = document.createElement('div');
        badgeContainer.style.marginTop = '6px';

        const checkingBadge = createReleaseBadge('checking');
        badgeContainer.appendChild(checkingBadge);

        insertTarget.parentNode.insertBefore(badgeContainer, insertTarget.nextSibling);

        const releaseInfo = await fetchReleaseInfo(owner, repo);

        badgeContainer.innerHTML = '';
        if (releaseInfo) {
            const releaseBadge = createReleaseBadge('has-release', releaseInfo);
            badgeContainer.appendChild(releaseBadge);
        } else {
            if (filterOnly) {
                item.style.display = 'none';
                badgeContainer.style.display = 'none';
            } else {
                const noReleaseBadge = createReleaseBadge('no-release');
                badgeContainer.appendChild(noReleaseBadge);
            }
        }
    }

    async function processSearchResults() {
        if (!window.location.pathname.startsWith('/search')) return;

        const params = new URLSearchParams(window.location.search);
        const filterOnly = params.get('userscript_has_release') === '1';

        const allItems = Array.from(document.querySelectorAll(CONFIG.selectors.resultItem));
        const itemsToProcess = allItems.filter(item => !item.dataset.releaseProcessed);

        itemsToProcess.forEach(item => item.dataset.releaseProcessed = 'true');

        await processQueue(itemsToProcess, 3, item => processItem(item, filterOnly));
    }

    /* =========================================================================
       UI: MODAL 
       ========================================================================= */
    let modalEl = null;
    let overlayEl = null;

    function toggleModal(show) {
        if (!modalEl) createUI();
        // If show is undefined, toggle. If defined, set.
        const visible = (show === undefined) ? (modalEl.dataset.visible !== 'true') : show;

        modalEl.dataset.visible = visible;
        overlayEl.dataset.visible = visible;

        if (visible) {
            loadStateToUI();
            const firstInput = modalEl.querySelector('input, select');
            if (firstInput) firstInput.focus();
        }
    }

    function createUI() {
        if (document.getElementById(CONFIG.ids.modal)) return;

        // Overlay
        overlayEl = document.createElement('div');
        overlayEl.className = 'brutal-overlay';
        overlayEl.onclick = () => toggleModal(false);
        document.body.appendChild(overlayEl);

        // Modal
        modalEl = document.createElement('div');
        modalEl.id = CONFIG.ids.modal;

        let html = `
            <div class="brutal-header">
                <span>FILTER</span>
                <span class="brutal-close" data-close>×</span>
            </div>
            <div class="brutal-body">
        `;

        FIELDS.forEach(section => {
            html += `<div class="brutal-section">
                <div class="brutal-section-title">${section.section}</div>
                <div class="brutal-grid ${section.items.length === 1 ? 'full' : ''}">`;

            section.items.forEach(field => {
                const dangerStyle = field.danger ? 'style="color:var(--brutal-accent)"' : '';
                html += `<div class="brutal-field">
                    <label ${dangerStyle}>${field.label}</label>
                    ${field.type === 'select'
                        ? `<select id="gh-field-${field.id}" class="brutal-input">${field.options.map(o => `<option value="${o.v}">${o.l}</option>`).join('')}</select>`
                        : `<input id="gh-field-${field.id}" type="text" class="brutal-input" placeholder="${field.placeholder || ''}">`}
                </div>`;
            });
            html += `</div></div>`;
        });

        html += `
            <div class="brutal-section">
                <div class="brutal-check-row">
                    <input type="checkbox" id="gh-field-releases" class="brutal-check">
                    <label for="gh-field-releases" class="brutal-check-label">Only with releases</label>
                </div>
            </div>
        </div>
        <div class="brutal-footer">
            <button data-clear class="brutal-btn">Clear</button>
            <button data-search class="brutal-btn primary">Search</button>
        </div>`;

        modalEl.innerHTML = html;
        document.body.appendChild(modalEl);

        // Floating toggle button
        if (!document.getElementById(CONFIG.ids.toggleBtn)) {
            const toggleBtn = document.createElement('button');
            toggleBtn.id = CONFIG.ids.toggleBtn;
            toggleBtn.type = 'button';
            toggleBtn.title = 'Search Filter';
            toggleBtn.innerHTML = `<svg viewBox="0 0 16 16"><path d="M10.68 11.74a6 6 0 1 1 1.06-1.06l3.04 3.04a.75.75 0 1 1-1.06 1.06l-3.04-3.04ZM11 6.5a4.5 4.5 0 1 0-9 0 4.5 4.5 0 0 0 9 0Z"/></svg>`;
            toggleBtn.onclick = () => toggleModal();
            document.body.appendChild(toggleBtn);
        }

        // Events
        modalEl.querySelector('[data-close]').onclick = () => toggleModal(false);
        modalEl.querySelector('[data-clear]').onclick = () => {
            modalEl.querySelectorAll('input, select').forEach(el => {
                el.type === 'checkbox' ? el.checked = false : el.value = '';
            });
        };
        modalEl.querySelector('[data-search]').onclick = executeSearch;

        // Keydown
        modalEl.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') executeSearch();
        });
        document.addEventListener('keydown', e => {
            if (e.key === 'Escape' && modalEl.dataset.visible === 'true') toggleModal(false);
        });
    }

    function loadStateToUI() {
        const state = QueryBuilder.parseCurrent();
        const setVal = (id, val) => {
            const el = document.getElementById(`gh-field-${id}`);
            if (el) el.value = val || '';
        };

        setVal('type', state.type);
        setVal('sort', state.sort);
        setVal('and', state.and);
        setVal('or', state.or);
        Object.entries(state.meta).forEach(([id, val]) => setVal(id, val));

        const relCheck = document.getElementById('gh-field-releases');
        if (relCheck) relCheck.checked = state.releasesOnly;
    }

    function executeSearch() {
        const getVal = id => document.getElementById(`gh-field-${id}`).value;
        const data = {
            type: getVal('type'),
            sort: getVal('sort'),
            and: getVal('and'),
            or: getVal('or'),
            meta: [],
            releasesOnly: document.getElementById('gh-field-releases').checked
        };

        FIELDS.find(s => s.section === 'META').items.forEach(item => {
            const val = getVal(item.id);
            if (val) data.meta.push({ key: item.meta, value: val });
        });

        window.location.href = QueryBuilder.buildUrl(data);
    }

    /* =========================================================================
       INIT
       ========================================================================= */
    function init() {
        injectStyles();
        createUI();

        // Register Menu
        if (typeof GM_registerMenuCommand === 'function') {
            GM_registerMenuCommand("Search Filter", () => toggleModal());
        }

        // Process existing results
        processSearchResults();

        // Observers
        let debounceTimer;
        // Watch for page changes (GitHub uses Turbo/PJAX)
        const observer = new MutationObserver(() => {
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(() => {
                processSearchResults();
            }, 200);
        });
        observer.observe(document.body, { childList: true, subtree: true });

        // Turbo event listener for cleaner navigation handling
        document.addEventListener('turbo:render', () => {
            processSearchResults();
        });
    }

    init();

})();