Save Your Searches

Save your favourite search queries for easy retrieval. Aims to enhance searching experience. Made for ImageBoard sites, but suitable for any websites with a search bar.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Save Your Searches
// @namespace    http://tampermonkey.net/
// @version      3.2
// @description  Save your favourite search queries for easy retrieval. Aims to enhance searching experience. Made for ImageBoard sites, but suitable for any websites with a search bar.
// @author       php
// @match        *://rule34.xxx/*
// @match        *://danbooru.donmai.us/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        unsafeWindow
// @require      https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const Sortable = window.Sortable;

    // ==========================================
    // [Module 1] EventBus
    // ==========================================
    const EventBus = {
        events: {},
        on(event, listener) {
            if (!this.events[event]) this.events[event] = [];
            this.events[event].push(listener);
        },
        emit(event, data) {
            if (this.events[event]) {
                this.events[event].forEach(listener => listener(data));
            }
        }
    };

    // ==========================================
    // [Module 2] Helper (The Single Source of Truth)
    // ==========================================
    const Helper = {
        // --- 2.1 Configuration Data ---
        Config: {
            DEFAULT_DATA: {
                settings: { version: "3.1.0", lastUpdate: new Date().toISOString() },
                profiles: {
                    "rule34.xxx": {
                        name: "Rule34",
                        selectors: { searchBar: "input[name='tags']", insertPoint: "div.tag-search" },
                        cheatsheetUrl: "https://rule34.xxx/index.php?page=help&topic=cheatsheet",
                        showCheatsheet: false,
                        wikiUrl: "https://rule34.xxx/index.php?page=wiki&s=list",
                        showWiki: false,
                        defaultOpenQueryBox: true,
                        showSettingsBtn: true,
                        delimiter: " ",
                        splitRegex: "\\s+",
                        groups: [
                            { id: "g1", label: "Sort", queries: ["sort:score:desc", "sort:id:desc"] },
                            { id: "g2", label: "Meta", queries: ["( video ~ animated )", "( 3d ~ 3d_animation )", "( ai ~ ai_generated ~ ai_assisted )"] }
                        ]
                    },
                    "danbooru.donmai.us": {
                        name: "Danbooru",
                        selectors: { searchBar: "#tags", insertPoint: "#search-box" },
                        cheatsheetUrl: "https://danbooru.donmai.us/wiki_pages/help:cheatsheet",
                        showCheatsheet: false,
                        wikiUrl: "https://danbooru.donmai.us/wiki_pages",
                        showWiki: false,
                        defaultOpenQueryBox: true,
                        showSettingsBtn: true,
                        delimiter: " ",
                        splitRegex: "\\s+",
                        groups: [
                            { id: "g3", label: "Sort", queries: ["order:favcount", "order:score", "order:id_desc", "age:..1w"] },
                            { id: "g4", label: "Meta", queries: ["~video ~animated", "~ai-generated ~ai-assisted"] }
                        ]
                    }
                }
            }
        },

        // --- 2.2 Styles Definition ---
        Styles: {
            PANEL_CSS: `
                .panel {
                    position: fixed;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    width: 95%;
                    max-width: 400px; /* Caps the size for desktop */
                    max-height: 90vh;
                    background: rgba(255,255,255,0.90);
                    backdrop-filter: blur(15px);
                    border-radius: 12px;
                    box-shadow: 0 10px 40px rgba(0,0,0,0.2);
                    z-index: 20000;
                    display: none;
                    flex-direction: column;
                    font-family: system-ui, sans-serif;
                    border: 1px solid rgba(0,0,0,0.1);
                }
                .header { padding: 15px; background: rgba(0,0,0,0.05); cursor: move; display: flex; justify-content: center; align-items: center; border-bottom: 1px solid rgba(0,0,0,0.1); font-weight: bold; }
                .content { padding: 15px; overflow-y: auto; flex: 1; }

                .footer { padding: 12px; display: flex; flex-direction: column; gap: 8px; background: #f9f9f9; border-top: 1px solid #eee; border-radius: 0 0 12px 12px; }
                .footer-row { display: flex; gap: 8px; width: 100%; }

                .section-title { font-size: 11px; font-weight: bold; color: #888; text-transform: uppercase; margin: 15px 0 8px 0; }
                .config-field { margin-bottom: 10px; }
                .config-field label { display: block; font-size: 11px; margin-bottom: 4px; color: #666; font-weight: 600; }
                .config-field input[type="text"], .config-field select { width: 100%; box-sizing: border-box; padding: 8px; border: 1px solid #ddd; border-radius: 6px; font-size: 13px; outline: none; }
                .checkbox-group { display: flex; flex-direction: column; gap: 8px; margin-top: 5px; }
                .checkbox-field { display: flex; align-items: center; gap: 6px; font-size: 12px; cursor: pointer; user-select: none; color: #444; }

                .card { background: white; padding: 12px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #eee; position: relative; }
                .sub-card { background: #f8f9fa; padding: 12px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #eaeced; position: relative; }
                .sub-card:last-child { margin-bottom: 0; } /* Removes extra space at the bottom of the container */
                .card-title { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }

                /* 幽靈輸入框樣式:平時像文字,互動時顯現 */
                .group-label-input {
                    background: transparent;
                    border: none;
                    /* Show a very faint line by default for mobile users */
                    border-bottom: 1px dashed rgba(0,0,0,0.1);
                    font-weight: bold;
                    font-size: 13px;
                    color: #444;
                    width: 100%;
                    outline: none;
                    transition: all 0.2s;
                    cursor: text;
                    padding: 2px 0;
                }

                .group-label-input:focus {
                    border-bottom: 1px solid #2196f3;
                    background: rgba(33, 150, 243, 0.05);
                }
                /* 排序模式下讓輸入框看起來像標籤,且不可點擊 */
                .group-label-input:disabled {
                    cursor: default;
                    border: none;
                }

                .tag-chip { display: inline-flex; align-items: center; background: #f0f0f0; padding: 4px 10px; border-radius: 6px; margin: 3px; font-size: 12px; border: 1px solid #e0e0e0; color: #000;}
                .tag-del { margin-left: 6px; color: #f44336; cursor: pointer; font-weight: bold; }

                .sortable-ghost { opacity: 0.4; border: 2px dashed #2196f3 !important; background: #e3f2fd !important; }

                .drag-handle-group {
                    cursor: grab;
                    color: #bbb;
                    font-size: 22px;
                    padding: 15px;
                    margin: -15px 0 -15px -15px; /* Negative margin offsets the padding */
                    display: inline-flex;
                    align-items: center;
                    justify-content: center;
                }

                .drag-handle-tag {
                    cursor: grab;
                    color: #ccc;
                    font-size: 18px;
                    padding: 10px;
                    margin: -10px 0 -10px -10px; /* Negative margin offsets the padding */
                    display: inline-flex;
                    align-items: center;
                    justify-content: center;
                }

                /* Change color when dragging to show it's "active" */
                .drag-handle-group:active, .drag-handle-tag:active {
                    color: #2196f3;
                }

                .btn { padding: 8px 16px; border-radius: 6px; cursor: pointer; border: none; font-size: 13px; font-weight: 500; transition: all 0.2s; display: inline-flex; align-items: center; justify-content: center; }
                .btn-add { background: #4caf50; color: white; margin-top: 5px; width: 100%; }
                .btn-current {
                    background: #ff9800;
                    color: white;
                }
                .btn:hover,
                .btn:active {
                    filter: brightness(0.9);
                }
                .btn-save { background: #2196f3; color: white; }

                /* [Modified] Rearrange button style adjusted for inline title placement */
                .btn-rearrange { background: #90a4ae; color: white; padding: 4px 10px; font-size: 11px; border-radius: 4px; }
                .btn-rearrange.active { background: #e91e63; }

                .btn-reset { background: #f44336; color: white; margin-right: auto; }
                .btn-data { background: #607d8b; color: white; flex: 1; font-size: 12px; padding: 6px; }

                .modal-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 20001; border-radius: 12px; }
                .modal-box { background: white; padding: 20px; border-radius: 10px; width: 280px; box-shadow: 0 4px 20px rgba(0,0,0,0.3); text-align: center; }
                .modal-title { font-size: 14px; font-weight: bold; margin-bottom: 15px; color: #333; }
                .modal-buttons { display: flex; flex-direction: column; gap: 10px; }
                .btn-modal { padding: 10px; border-radius: 6px; cursor: pointer; border: none; font-size: 13px; font-weight: bold; text-align: center; transition: opacity 0.2s; }
                .btn-modal:hover { opacity: 0.8; }
                .btn-modal-current { background: #2196f3; color: white; }
                .btn-modal-all { background: #4caf50; color: white; }
                .btn-modal-cancel { background: #f44336; color: white; }
                .warning-box { background: #fff3cd; color: #856404; padding: 10px; border-radius: 8px; border: 1px solid #ffeeba; margin-bottom: 15px; font-size: 12px; line-height: 1.4; }
            `,
            INJECTOR_WRAPPER: "margin: 10px 0; font-family: sans-serif; display: flex; gap: 8px; flex-wrap: wrap; justify-content: center;",
            TRIGGER_BTN: "padding: 4px 6px; background: #6c757d; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500;",
            SETTINGS_BTN: "padding: 4px 6px; background: #3f51b5; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500;",
            CHEATSHEET_BTN: "padding: 4px 6px; background: #009688; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; text-decoration: none; font-weight: 500;",
            WIKI_BTN: "padding: 4px 6px; background: #ff5722; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; text-decoration: none; font-weight: 500;",
            QUERY_BOX: "width: 100%; margin-top: 5px; display: flex; flex-direction: column; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.05);",
            GROUP_CARD: (isLast) => `padding: 12px; border-bottom: ${isLast ? 'none' : '2px solid #eee'}`,
            GROUP_TITLE: "font-size: 11px; font-weight: bold; color: #888; text-transform: uppercase; margin-bottom: 8px;",

            TAG_ITEM: "background: white; color: #000; border: 1px solid #ced4da; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 14px; transition: all 0.15s ease;",
            TAG_ITEM_ACTIVE: "background: #e3f2fd; border: 1px solid #2196f3; color: #0d47a1; padding: 8px 12px; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: bold; transition: all 0.15s ease;"
        },

        // --- 2.3 HTML UI Components ---
        Templates: {
            PANEL_SHELL: (contentHTML) => `
                <style>${Helper.Styles.PANEL_CSS}</style>
                <div class="panel" id="main-panel">
                    <div class="header" id="drag-handle">Save Your Searches</div>
                    <div class="content" id="settings-content">${contentHTML}</div>

                    <div class="footer">
                        <div class="footer-row">
                            <button class="btn btn-data" id="btn-export-trigger">📤 Export JSON</button>
                            <button class="btn btn-data" id="btn-import-trigger">📥 Import JSON</button>
                            <input type="file" id="sys-import-file" style="display:none" accept=".json">
                        </div>
                        <div class="footer-row">
                            <button class="btn btn-reset" id="btn-reset">Reset</button>
                            <button class="btn" id="btn-cancel">Cancel</button>
                            <button class="btn btn-save" id="btn-save">Save & Apply</button>
                        </div>
                    </div>

                    <div class="modal-overlay" id="action-modal">
                        <div class="modal-box">
                            <div class="modal-title" id="modal-title">Select Scope</div>
                            <div class="modal-buttons">
                                <button class="btn-modal btn-modal-current" id="modal-btn-current">Only Current Website</button>
                                <button class="btn-modal btn-modal-all" id="modal-btn-all">All Websites</button>
                                <button class="btn-modal btn-modal-cancel" id="modal-btn-cancel">Cancel</button>
                            </div>
                        </div>
                    </div>
                </div>
            `,
            PROFILE_SECTION: (profiles, currentHost) => `
                <div class="section-title">Profile (Website)</div>
                <div class="config-field">
                    <select id="profile-selector">
                        ${Object.keys(profiles).map(h => `<option value="${h}" ${h === currentHost ? 'selected' : ''}>${h}</option>`).join('')}
                    </select>
                </div>
            `,
            SITE_SELECTORS_SECTION: (profile, isRearranging) => `
                <div class="section-title">Site Selectors & Tag Delimiter</div>
                <div class="card">
                    <div class="config-field"><label>Search Bar Selector</label><input type="text" value="${profile.selectors.searchBar}" id="input-sel-search" ${isRearranging?'disabled':''}></div>
                    <div class="config-field"><label>Insert Point Selector</label><input type="text" value="${profile.selectors.insertPoint}" id="input-sel-insert" ${isRearranging?'disabled':''}></div>

                    <hr style="border: 0; border-top: 1px solid #eee; margin: 12px 0;">

                    <div style="display:flex; gap:10px;">
                        <div class="config-field" style="flex:1;"><label>Tag Delimiter Character</label><input type="text" value="${profile.delimiter}" id="input-delimiter" ${isRearranging?'disabled':''}></div>
                        <div class="config-field" style="flex:1;"><label>Tag Delimiter Regex</label><input type="text" value="${profile.splitRegex}" id="input-split-regex" ${isRearranging?'disabled':''}></div>
                    </div>
                </div>
            `,
            SITE_RESOURCES_SECTION: (profile, isRearranging) => `
                <div class="section-title">Site Resources & Buttons</div>
                <div class="card">
                    <div class="config-field"><label>Cheatsheet URL</label><input type="text" value="${profile.cheatsheetUrl || ''}" id="input-cheatsheet-url" ${isRearranging?'disabled':''}></div>
                    <div class="config-field"><label>Wiki URL</label><input type="text" value="${profile.wikiUrl || ''}" id="input-wiki-url" ${isRearranging?'disabled':''}></div>

                    <hr style="border: 0; border-top: 1px solid #eee; margin: 12px 0;">

                    <div class="checkbox-group">
                        <label class="checkbox-field"><input type="checkbox" id="check-show-cheatsheet" ${profile.showCheatsheet ? 'checked' : ''} ${isRearranging?'disabled':''}> Show Cheatsheet</label>
                        <label class="checkbox-field"><input type="checkbox" id="check-show-wiki" ${profile.showWiki ? 'checked' : ''} ${isRearranging?'disabled':''}> Show Wiki</label>
                        <label class="checkbox-field"><input type="checkbox" id="check-show-settings" ${profile.showSettingsBtn !== false ? 'checked' : ''} ${isRearranging?'disabled':''}> Show Settings Button</label>
                        <label class="checkbox-field"><input type="checkbox" id="check-default-open" ${profile.defaultOpenQueryBox ? 'checked' : ''} ${isRearranging?'disabled':''}> Open Saved Searches Box by Default</label>
                    </div>
                </div>
            `,
            TAG_GROUP_HTML: (group, gIdx, isRearranging) => `
                <div class="sub-card sortable-group-item" data-gidx="${gIdx}">
                    <div class="card-title">
                        <div style="display:flex; align-items:center; flex:1;">
                            ${isRearranging ? `<span class="drag-handle-group">☰</span>` : ''}


                            <input type="text"
                                   value="${group.label}"
                                   class="group-label-input"
                                   style="margin-left: 10px;"
                                   data-gidx="${gIdx}"
                                   ${isRearranging ? 'disabled' : ''}
                                   placeholder="Group Title">

                        </div>
                        ${!isRearranging ? `<span class="tag-del delete-group" data-gidx="${gIdx}">✖</span>` : ''}
                    </div>
                    <div style="display:flex; flex-wrap:wrap; min-height: 20px;" class="tags-sortable-container" data-gidx="${gIdx}">
                        ${group.queries.map((q, qIdx) => `
                            <span class="tag-chip sortable-tag-item" data-qidx="${qIdx}">
                                ${isRearranging ? `<span class="drag-handle-tag">⋮</span>` : ''}
                                ${q}
                                ${!isRearranging ? `<span class="tag-del delete-tag" data-gidx="${gIdx}" data-qidx="${qIdx}">×</span>` : ''}
                            </span>
                        `).join('')}
                    </div>
                    ${!isRearranging ? `
                        <div style="display:flex; gap:5px; margin-top:10px;">
                            <input type="text" placeholder="Add tag..." class="new-tag-input" data-gidx="${gIdx}" style="flex:1; padding:4px;">
                            <button class="btn btn-save add-tag-btn" data-gidx="${gIdx}" style="padding:4px 12px;">+</button>
                            <button class="btn btn-current add-current-query-btn" data-gidx="${gIdx}" title="Add current search bar content" style="padding:4px 8px;">+ Current</button>
                        </div>
                    ` : ''}
                </div>
            `,
            NEW_PROFILE_WARNING: `
                <div class="warning-box">
                    <strong>⚠️ New Website Detected!</strong><br>
                    This website doesn't have a profile yet. Please click <b>"Save & Apply"</b> to save settings for this site.
                </div>
            `,
            SAVED_SEARCHES_HEADER: (isRearranging) => `
                <div class="section-title" style="display:flex; justify-content:space-between; align-items:center;">
                    <span>Saved Searches ${isRearranging ? '(Rearrange Mode)' : ''}</span>
                    <button class="btn btn-rearrange" id="btn-rearrange">⇅ Rearrange</button>
                </div>
                <div id="cards-container" class="card"></div>
            `
        }
    };

    // ==========================================
    // [Module 3] Storage
    // ==========================================
    const Storage = {
        STORAGE_KEY: "SaveYourSearches_Data",
        load() {
            const rawData = GM_getValue(this.STORAGE_KEY, null);
            if (!rawData) { this.save(Helper.Config.DEFAULT_DATA); return Helper.Config.DEFAULT_DATA; }
            try {
                const data = JSON.parse(rawData);
                Object.keys(Helper.Config.DEFAULT_DATA.profiles).forEach(key => {
                    if (!data.profiles[key]) {
                        data.profiles[key] = Helper.Config.DEFAULT_DATA.profiles[key];
                    } else {
                        const defaultProf = Helper.Config.DEFAULT_DATA.profiles[key];
                        ["cheatsheetUrl", "showCheatsheet", "wikiUrl", "showWiki", "defaultOpenQueryBox", "delimiter", "splitRegex", "showSettingsBtn"].forEach(field => {
                            if (data.profiles[key][field] === undefined) data.profiles[key][field] = defaultProf[field];
                        });
                    }
                });
                return data;
            } catch (e) { return Helper.Config.DEFAULT_DATA; }
        },
        save(data) {
            data.settings.lastUpdate = new Date().toISOString();
            GM_setValue(this.STORAGE_KEY, JSON.stringify(data));
            EventBus.emit('storage:updated', data);
        }
    };

    // ==========================================
    // [Module 4] UIEngine
    // ==========================================
    const UIEngine = {
        container: null, shadowRoot: null, panel: null, tempData: null, editingHost: null,
        isRearranging: false, sortableInstances: [],

        init() {
            this.container = document.createElement('div');
            this.container.id = 'sys-ui-root';
            document.body.appendChild(this.container);
            this.shadowRoot = this.container.attachShadow({ mode: 'open' });
            this.shadowRoot.innerHTML = Helper.Templates.PANEL_SHELL("");
            this.panel = this.shadowRoot.getElementById('main-panel');
            this.setupPanelDragging();
            this.bindBaseButtons(); // Binds static footer buttons
            EventBus.on('storage:updated', () => this.refreshSettings());
        },

        isNewProfile: false,

        toggle() {
            if (getComputedStyle(this.panel).display === 'none') {
                this.tempData = JSON.parse(JSON.stringify(Storage.load()));
                this.editingHost = window.location.host;
                this.isRearranging = false;

                if (!this.tempData.profiles[this.editingHost]) {
                    this.tempData.profiles[this.editingHost] = {
                        name: this.editingHost,
                        selectors: { searchBar: "", insertPoint: "" },
                        cheatsheetUrl: "", showCheatsheet: false,
                        wikiUrl: "", showWiki: false,
                        defaultOpenQueryBox: false,
                        delimiter: " ",
                        splitRegex: "\\s+",
                        groups: []
                    };
                }

                this.refreshSettings();
                this.panel.style.display = 'flex';

            } else {
                this.panel.style.display = 'none';
            }
        },

        refreshSettings() {
            this.sortableInstances.forEach(i => i.destroy());
            this.sortableInstances = [];

            const storedData = Storage.load();
            this.isNewProfile = !storedData.profiles[this.editingHost];

            const content = this.shadowRoot.getElementById('settings-content');
            const profile = this.tempData.profiles[this.editingHost];

            let html = this.isNewProfile ? Helper.Templates.NEW_PROFILE_WARNING : "";
            html += Helper.Templates.PROFILE_SECTION(this.tempData.profiles, this.editingHost);
            html += Helper.Templates.SITE_SELECTORS_SECTION(profile, this.isRearranging);
            html += Helper.Templates.SITE_RESOURCES_SECTION(profile, this.isRearranging);
            html += Helper.Templates.SAVED_SEARCHES_HEADER(this.isRearranging);

            if (!this.isRearranging) html += `<button class="btn btn-add" id="btn-add-group">+ Add New Group</button>`;

            content.innerHTML = html;

            const container = this.shadowRoot.getElementById('cards-container');
            profile.groups.forEach((group, gIdx) => {
                const cardDiv = document.createElement('div');
                cardDiv.innerHTML = Helper.Templates.TAG_GROUP_HTML(group, gIdx, this.isRearranging);
                container.appendChild(cardDiv.firstElementChild);
            });

            this.bindDynamicEvents();
            if (this.isRearranging) this.setupSortable();
        },

        setupSortable() {
            const profile = this.tempData.profiles[this.editingHost];
            const groupContainer = this.shadowRoot.getElementById('cards-container');
            this.sortableInstances.push(new Sortable(groupContainer, {
                animation: 180, handle: '.drag-handle-group', ghostClass: 'sortable-ghost',
                onEnd: (evt) => {
                    const movedGroup = profile.groups.splice(evt.oldIndex, 1)[0];
                    profile.groups.splice(evt.newIndex, 0, movedGroup);
                }
            }));

            this.shadowRoot.querySelectorAll('.tags-sortable-container').forEach(el => {
                this.sortableInstances.push(new Sortable(el, {
                    animation: 180,
                    handle: '.drag-handle-tag',
                    ghostClass: 'sortable-ghost',
                    group: 'shared-tags', // [Changed] Common group name allows cross-list dragging
                    onEnd: (evt) => {
                        // [Changed] Identify source and destination group indices
                        const fromGidx = evt.from.dataset.gidx;
                        const toGidx = evt.to.dataset.gidx;

                        // [Changed] Target the specific arrays
                        const fromQueries = profile.groups[fromGidx].queries;
                        const toQueries = profile.groups[toGidx].queries;

                        // [Changed] Move the item from the old array to the new array
                        const movedQuery = fromQueries.splice(evt.oldIndex, 1)[0];
                        toQueries.splice(evt.newIndex, 0, movedQuery);

                        // [Changed] Refresh UI if moved to a new group to update HTML indices
                        if (fromGidx !== toGidx) {
                            this.refreshSettings();
                        }
                    }
                }));
            });
        },

        showActionModal(titleText, onCurrent, onAll) {
            const modal = this.shadowRoot.getElementById('action-modal');
            const title = this.shadowRoot.getElementById('modal-title');
            const btnCurrent = this.shadowRoot.getElementById('modal-btn-current');
            const btnAll = this.shadowRoot.getElementById('modal-btn-all');
            const btnCancel = this.shadowRoot.getElementById('modal-btn-cancel');

            title.innerText = titleText;
            modal.style.display = 'flex';

            const closeModal = () => {
                modal.style.display = 'none';
                btnCurrent.onclick = null;
                btnAll.onclick = null;
                btnCancel.onclick = null;
            };

            btnCurrent.onclick = () => { onCurrent(); closeModal(); };
            btnAll.onclick = () => { onAll(); closeModal(); };
            btnCancel.onclick = closeModal;
        },

        handleExport() {
            this.showActionModal(
                "Select Export Scope",
                () => {
                    const dataToExport = this.tempData.profiles[this.editingHost];
                    this.executeExport(dataToExport, 'SINGLE');
                },
                () => {
                    const dataToExport = this.tempData;
                    this.executeExport(dataToExport, 'ALL');
                }
            );
        },

        executeExport(dataToExport, mode) {
            const blob = new Blob([JSON.stringify(dataToExport, null, 2)], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;

            const fileNamePrefix = mode === 'SINGLE' ? `Save_Your_Searches_backup_${this.editingHost}` : `Save_Your_Searches_backup_ALL`;
            a.download = `${fileNamePrefix}_${new Date().getTime()}.json`;

            a.click();
            URL.revokeObjectURL(url);
            window.alert("Export complete!");
        },

        handleImport(event) {
            const file = event.target.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = (e) => {
                try {
                    const imported = JSON.parse(e.target.result);

                    this.showActionModal(
                        "Import Data (Will overwrite existing)",
                        () => {
                            const profileToImport = imported.profiles ? imported.profiles[this.editingHost] : imported;
                            if (!profileToImport || !profileToImport.groups) throw new Error("Invalid profile format");
                            this.tempData.profiles[this.editingHost] = profileToImport;
                            window.alert("Import success! Click 'Save & Apply' to take effect.");
                            this.refreshSettings();
                        },
                        () => {
                            if (!imported.profiles) throw new Error("Invalid format for full backup");
                            this.tempData = imported;
                            window.alert("Import success! Click 'Save & Apply' to take effect.");
                            this.refreshSettings();
                        }
                    );
                } catch (err) { window.alert("Import Failed: " + err.message); }
            };
            reader.readAsText(file);
            event.target.value = '';
        },

        bindDynamicEvents() {
            const profile = this.tempData.profiles[this.editingHost];

            // [Modified] Rearrange button is now generated inside settings-content dynamically
            const btnRearrange = this.shadowRoot.getElementById('btn-rearrange');
            if (btnRearrange) {
                btnRearrange.classList.toggle('active', this.isRearranging);
                btnRearrange.onclick = () => { this.isRearranging = !this.isRearranging; this.refreshSettings(); };
            }

            if (this.isRearranging) return;

            this.shadowRoot.getElementById('profile-selector').onchange = (e) => { this.editingHost = e.target.value; this.refreshSettings(); };
            this.shadowRoot.getElementById('input-sel-search').oninput = (e) => { profile.selectors.searchBar = e.target.value; };
            this.shadowRoot.getElementById('input-sel-insert').oninput = (e) => { profile.selectors.insertPoint = e.target.value; };

            this.shadowRoot.getElementById('input-delimiter').oninput = (e) => { profile.delimiter = e.target.value; };
            this.shadowRoot.getElementById('input-split-regex').oninput = (e) => { profile.splitRegex = e.target.value; };

            this.shadowRoot.getElementById('input-cheatsheet-url').oninput = (e) => { profile.cheatsheetUrl = e.target.value; };
            this.shadowRoot.getElementById('check-show-cheatsheet').onchange = (e) => { profile.showCheatsheet = e.target.checked; };
            this.shadowRoot.getElementById('input-wiki-url').oninput = (e) => { profile.wikiUrl = e.target.value; };
            this.shadowRoot.getElementById('check-show-wiki').onchange = (e) => { profile.showWiki = e.target.checked; };
            this.shadowRoot.getElementById('check-show-settings').onchange = (e) => { profile.showSettingsBtn = e.target.checked; }; // [NEW]
            this.shadowRoot.getElementById('check-default-open').onchange = (e) => { profile.defaultOpenQueryBox = e.target.checked; };

            this.shadowRoot.querySelectorAll('.delete-tag').forEach(btn => {
                btn.onclick = (e) => {
                    const { gidx, qidx } = e.target.dataset;
                    profile.groups[gidx].queries.splice(qidx, 1);
                    this.refreshSettings();
                };
            });
            this.shadowRoot.querySelectorAll('.add-tag-btn').forEach(btn => {
                btn.onclick = (e) => {
                    const gidx = e.target.dataset.gidx;
                    const val = this.shadowRoot.querySelectorAll('.new-tag-input')[gidx].value.trim();
                    if (val) { profile.groups[gidx].queries.push(val); this.refreshSettings(); }
                };
            });

            this.shadowRoot.querySelectorAll('.add-current-query-btn').forEach(btn => {
                btn.onclick = (e) => {
                    const gidx = e.target.dataset.gidx;
                    const bar = document.querySelector(profile.selectors.searchBar);

                    if (bar && bar.value.trim()) {
                        const fullQuery = bar.value.trim(); // Grab the entire search bar content

                        if (!profile.groups[gidx].queries.includes(fullQuery)) {
                            profile.groups[gidx].queries.push(fullQuery);
                            this.refreshSettings();
                        }
                    }
                };
            });

            this.shadowRoot.querySelectorAll('.group-label-input').forEach(input => {
                input.onchange = (e) => { profile.groups[e.target.dataset.gidx].label = e.target.value; };
            });
            this.shadowRoot.querySelectorAll('.delete-group').forEach(btn => {
                btn.onclick = (e) => { if(confirm("Delete this group?")) { profile.groups.splice(e.target.dataset.gidx, 1); this.refreshSettings(); } };
            });
            this.shadowRoot.getElementById('btn-add-group').onclick = () => {
                profile.groups.push({ id: "g_"+Date.now(), label: "New Group", queries: [] });
                this.refreshSettings();
            };
        },

        // [Modified] Binds static elements located in the base PANEL_SHELL template
        bindBaseButtons() {
            // Data row buttons
            this.shadowRoot.getElementById('btn-export-trigger').onclick = () => this.handleExport();
            this.shadowRoot.getElementById('btn-import-trigger').onclick = () => this.shadowRoot.getElementById('sys-import-file').click();
            this.shadowRoot.getElementById('sys-import-file').onchange = (e) => this.handleImport(e);

            // Action row buttons
            this.shadowRoot.getElementById('btn-cancel').onclick = () => this.toggle();
            this.shadowRoot.getElementById('btn-save').onclick = () => { Storage.save(this.tempData); this.toggle(); };
            this.shadowRoot.getElementById('btn-reset').onclick = () => { if(confirm("Reset ALL data?")) { Storage.save(Helper.Config.DEFAULT_DATA); this.toggle(); } };
        },

        setupPanelDragging() {
            const handle = this.shadowRoot.getElementById('drag-handle');
            let startX, startY, initialLeft, initialTop;
            const onMouseMove = (e) => {
                this.panel.style.left = `${initialLeft + (e.clientX - startX)}px`;
                this.panel.style.top = `${initialTop + (e.clientY - startY)}px`;
            };
            const onMouseUp = () => {
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
            };
            handle.onmousedown = (e) => {
                if (e.button !== 0) return;
                startX = e.clientX; startY = e.clientY;
                initialLeft = this.panel.offsetLeft; initialTop = this.panel.offsetTop;
                document.addEventListener('mousemove', onMouseMove);
                document.addEventListener('mouseup', onMouseUp);
            };
        }
    };

    // ==========================================
    // [Module 5] SiteInjector
    // ==========================================
    const SiteInjector = {
        async init() {
            const host = window.location.host;
            let data = Storage.load();
            if (data.profiles[host]) this.injectUI(data.profiles[host]);
            EventBus.on('storage:updated', (newData) => { if (newData.profiles[host]) this.injectUI(newData.profiles[host]); });
        },

        async injectUI(profile) {
            const oldUI = document.getElementById('sys-wrapper');
            if (oldUI) oldUI.remove();
            if (!profile.selectors.insertPoint) return;
            const target = await this.waitForElement(profile.selectors.insertPoint);
            if (!target) return;

            const wrapper = document.createElement('div');
            wrapper.id = 'sys-wrapper';
            wrapper.style = Helper.Styles.INJECTOR_WRAPPER;

            const triggerBtn = document.createElement('button');
            triggerBtn.innerText = "🔍 Saved Searches";
            triggerBtn.style = Helper.Styles.TRIGGER_BTN;

            // Settings Button Logic
            let settingsBtn = null;
            if (profile.showSettingsBtn !== false) { // Defaults to true if undefined
                settingsBtn = document.createElement('button');
                settingsBtn.innerText = "⚙️";
                settingsBtn.style = Helper.Styles.SETTINGS_BTN;
                settingsBtn.onclick = (e) => {
                    e.preventDefault();
                    UIEngine.toggle(); // Opens the settings panel
                };
            }

            let cheatsheetBtn = null;
            if (profile.showCheatsheet && profile.cheatsheetUrl) {
                cheatsheetBtn = document.createElement('a');
                cheatsheetBtn.innerText = "📄 Cheatsheet";
                cheatsheetBtn.href = profile.cheatsheetUrl;
                cheatsheetBtn.target = "_blank";
                cheatsheetBtn.style = Helper.Styles.CHEATSHEET_BTN;
            }

            let wikiBtn = null;
            if (profile.showWiki && profile.wikiUrl) {
                wikiBtn = document.createElement('a');
                wikiBtn.innerText = "🌐 Wiki";
                wikiBtn.href = profile.wikiUrl;
                wikiBtn.target = "_blank";
                wikiBtn.style = Helper.Styles.WIKI_BTN;
            }

            const queryBox = this.createBox(profile);

            const shouldBeOpen = profile.defaultOpenQueryBox === true;
            queryBox.style.display = shouldBeOpen ? "block" : "none";
            triggerBtn.style.background = shouldBeOpen ? "#2196f3" : "#6c757d";

            triggerBtn.onclick = (e) => {
                e.preventDefault();
                const isHidden = queryBox.style.display === "none";
                queryBox.style.display = isHidden ? "block" : "none";
                triggerBtn.style.background = isHidden ? "#2196f3" : "#6c757d";
            };

            wrapper.appendChild(triggerBtn);
            if (settingsBtn) wrapper.appendChild(settingsBtn);
            if (cheatsheetBtn) wrapper.appendChild(cheatsheetBtn);
            if (wikiBtn) wrapper.appendChild(wikiBtn);
            wrapper.appendChild(queryBox);

            target.parentNode.insertBefore(wrapper, target.nextSibling);

            const searchBar = document.querySelector(profile.selectors.searchBar);
            if (searchBar) {
                searchBar.addEventListener('input', () => this.updateHighlights(profile));
                searchBar.addEventListener('change', () => this.updateHighlights(profile));
                this.updateHighlights(profile);
            }
        },

        createBox(profile) {
            const box = document.createElement('div');
            box.id = 'sys-query-box';
            box.style = Helper.Styles.QUERY_BOX;

            profile.groups.forEach((group, index) => {
                if (group.queries.length === 0) return;
                const card = document.createElement('div');
                card.style = Helper.Styles.GROUP_CARD(index === profile.groups.length - 1);
                const title = document.createElement('div');
                title.innerText = group.label;
                title.style = Helper.Styles.GROUP_TITLE;
                const tagsCont = document.createElement('div');
                tagsCont.style = "display: flex; flex-wrap: wrap; gap: 6px;";

                group.queries.forEach(query => {
                    const tag = document.createElement('span');
                    tag.innerText = query;
                    tag.style = Helper.Styles.TAG_ITEM;
                    tag.setAttribute('data-sys-query', query);

                    tag.onclick = () => this.toggleQuery(profile, query);
                    tagsCont.appendChild(tag);
                });

                card.appendChild(title);
                card.appendChild(tagsCont);
                box.appendChild(card);
            });
            return box;
        },


        _escapeRegex(string) {
            return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        },

        // 替換原本的 toggleQuery 函式
        toggleQuery(profile, query) {
            const bar = document.querySelector(profile.selectors.searchBar);
            if (!bar) return;

            const delimiterStr = profile.delimiter !== undefined ? profile.delimiter : " ";
            const delimiterRegexStr = profile.splitRegex || "\\s+";
            const escapedQuery = this._escapeRegex(query);

            // 建立邊界比對 Regex:尋找 (開頭或分隔符) + 標籤 + (結尾或分隔符)
            // 使用 $1 捕捉前面的邊界,避免刪除時把前面的分隔符也吃掉
            const matchRegex = new RegExp(`(^|${delimiterRegexStr})${escapedQuery}(?=${delimiterRegexStr}|$)`, 'g');

            if (matchRegex.test(bar.value)) {
                // 如果標籤存在,將其移除。
                // 將標籤替換為它前面的邊界符號 ($1),接著清理多餘的連續分隔符號
                bar.value = bar.value.replace(matchRegex, '$1')
                                     .replace(new RegExp(`(${delimiterRegexStr})+`, 'g'), delimiterStr)
                                     .trim();

                if (bar.value.length > 0) bar.value += delimiterStr; // 保持結尾有空格的習慣
            } else {
                // 如果標籤不存在,將其加到最後
                let currentVal = bar.value.trim();
                bar.value = (currentVal ? currentVal + delimiterStr : "") + query + delimiterStr;
            }

            bar.dispatchEvent(new Event('input', { bubbles: true }));
        },

        // 替換原本的 updateHighlights 函式
        updateHighlights(profile) {
            const bar = document.querySelector(profile.selectors.searchBar);
            if (!bar) return;

            const box = document.getElementById('sys-query-box');
            if (!box) return;

            const delimiterRegexStr = profile.splitRegex || "\\s+";

            box.querySelectorAll('span[data-sys-query]').forEach(tagEl => {
                const query = tagEl.getAttribute('data-sys-query');
                const escapedQuery = this._escapeRegex(query);

                // 使用相同的邊界比對邏輯測試該標籤是否存在於搜尋列中
                const matchRegex = new RegExp(`(^|${delimiterRegexStr})${escapedQuery}(?=${delimiterRegexStr}|$)`);

                const isMatched = matchRegex.test(bar.value);

                tagEl.style.cssText = isMatched ? Helper.Styles.TAG_ITEM_ACTIVE : Helper.Styles.TAG_ITEM;
            });
        },

        waitForElement(selector) {
            return new Promise(resolve => {
                if (document.querySelector(selector)) return resolve(document.querySelector(selector));
                const obs = new MutationObserver(() => {
                    if (document.querySelector(selector)) { obs.disconnect(); resolve(document.querySelector(selector)); }
                });
                obs.observe(document.body, { childList: true, subtree: true });
            });
        }
    };

    // ==========================================
    // Boot Initialization
    // ==========================================
    UIEngine.init();
    SiteInjector.init();
    GM_registerMenuCommand("Open Settings Panel", () => UIEngine.toggle());
    const targetWindow = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
    targetWindow.toggleQM = () => UIEngine.toggle();

})();