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();

})();