Stylish Search Popup + Sites Manager

Popup under cursor (if fits), hide on click any button, preload enabled icons on page load, 5-per-row, /favicon.ico with fallback; add/remove sites in Settings. Now with "reuse existing tab", "toggle copy button", "auto-copy on select" and import/export features!

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Stylish Search Popup + Sites Manager
// @namespace    http://tampermonkey.net/
// @version      2025.11.02
// @author       TesterTV
// @homepageURL  https://github.com/testertv/StylishSearchPopup
// @license      GPL v.3 or any later version.
// @namespace    https://greasyfork.org/ru/scripts/554584-stylish-search-popup-sites-manager
// @description  Popup under cursor (if fits), hide on click any button, preload enabled icons on page load, 5-per-row, /favicon.ico with fallback; add/remove sites in Settings. Now with "reuse existing tab", "toggle copy button", "auto-copy on select" and import/export features!
// @match        *://*/*
// @grant        GM_addStyle
// @grant        GM_openInTab
// @grant        GM_registerMenuCommand
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM_setClipboard
// @grant        GM.addValueChangeListener
// ==/UserScript==

(async function () {
  'use strict';

  const POPUP_ID = 'stylish-search-popup-container';
  const SETTINGS_OVERLAY_ID = 'ssp-settings-overlay';
  const SETTINGS_MODAL_ID = 'ssp-settings-modal';
  const STORAGE_KEYS = {
    enabledEngineKeys: 'ssp_enabledEngineKeys',
    engines: 'ssp_engines',
    reuseTab: 'ssp_reuseTab',
    showCopyButton: 'ssp_showCopyButton',
    autoCopyOnSelect: 'ssp_autoCopyOnSelect', // New storage key
    searchRequest: 'ssp_searchRequest',
  };

  const DEFAULT_ENGINES = [
    { key: 'google',     name: 'Google',     iconHost: 'google.com',     url: 'https://www.google.com/search?q={query}' },
    { key: 'duckduckgo', name: 'DuckDuckGo', iconHost: 'duckduckgo.com',      url: 'https://duckduckgo.com/?q={query}' },
    { key: 'startpage',  name: 'Startpage',  iconHost: 'startpage.com',   url: 'https://www.startpage.com/search?q={query}' },
	{ key: 'bing',       name: 'Bing',       iconHost: 'bing.com',        url: 'https://www.bing.com/search?q={query}' },
	{ key: 'yahoo',      name: 'Yahoo',      iconHost: 'yahoo.com',       url: 'https://search.yahoo.com/search?p={query}' },
	{ key: 'brave',     name: 'Brave',     iconHost: 'brave.com',           url: 'https://search.brave.com/search?q={query}&source=web' },
	{ key: 'yandex',     name: 'Yandex',     iconHost: 'yandex.com',           url: 'https://yandex.com/search/?text={query}' },
    { key: 'youtube',    name: 'YouTube',    iconHost: 'youtube.com',     url: 'https://www.youtube.com/results?search_query={query}' },
	{ key: 'tiktok',     name: 'TikTok',     iconHost: 'tiktok.com',      url: 'https://www.tiktok.com/search?q={query}' },
    { key: 'twitch',     name: 'Twitch',     iconHost: 'twitch.tv',       url: 'https://www.twitch.tv/search?term={query}' },
    { key: 'deepl',    name: 'Deepl (De->Ru)',    iconHost: 'deepl.com',     url: 'https://www.deepl.com/en/translator#de/ru/{query}' },
    { key: 'gtranslate',    name: 'Google Translate (Auto->Ru)',    iconHost: 'translate.google.com',     url: 'https://translate.google.com/?sl=auto&tl=ru&text={query}&op=translate' },
    { key: 'facebook',   name: 'Facebook',   iconHost: 'facebook.com',    url: 'https://www.facebook.com/search/top/?q={query}' },
    { key: 'instagram',  name: 'Instagram',  iconHost: 'instagram.com',   url: 'https://www.instagram.com/explore/search/keyword/?q={query}' },
    { key: 'x',  name: 'X',  iconHost: 'x.com',   url: 'https://x.com/search?q={query}' },
    { key: 'wikipedia',  name: 'Wikipedia',  iconHost: 'wikipedia.org',   url: 'https://www.wikipedia.org/search-redirect.php?search={query}' },
    { key: 'reddit',     name: 'Reddit',     iconHost: 'reddit.com',      url: 'https://www.reddit.com/search/?q={query}' },
    { key: 'amazon',     name: 'Amazon',     iconHost: 'amazon.com',      url: 'https://www.amazon.com/s?k={query}' },
    { key: 'temu',       name: 'Temu',       iconHost: 'temu.com',        url: 'https://www.temu.com/search_result.html?search_key={query}' },
    { key: 'ebay',       name: 'eBay',       iconHost: 'ebay.com',        url: 'https://www.ebay.com/sch/i.html?_nkw={query}' },
    { key: 'walmart',       name: 'Walmart',       iconHost: 'walmart.com',        url: 'https://www.walmart.com/search/?query={query}' },
    { key: 'aliexpress',       name: 'AliExpress',       iconHost: 'aliexpress.com',        url: 'https://www.aliexpress.com/wholesale?SearchText={query}' },
    { key: 'netflix',    name: 'Netflix',    iconHost: 'netflix.com',     url: 'https://www.netflix.com/search?q={query}' },
    { key: 'imdb',    name: 'IMDb',    iconHost: 'imdb.com',     url: 'https://www.imdb.com/find/?q={query}' },
    { key: 'kinopoisk',    name: 'Кинопоиск',    iconHost: 'kinopoisk.ru',     url: 'https://www.kinopoisk.ru/index.php?kp_query={query}' },
	{ key: 'fandom',     name: 'Fandom',     iconHost: 'fandom.com',      url: 'https://www.fandom.com/?s={query}' },
    { key: 'pinterest',  name: 'Pinterest',  iconHost: 'pinterest.com',   url: 'https://www.pinterest.com/search/pins/?q={query}' },
    { key: 'github',     name: 'GitHub',     iconHost: 'github.com',          url: 'https://github.com/search?q={query}' },
    { key: 'stackoverflow',     name: 'Stack Overflow',     iconHost: 'stackoverflow.com',          url: 'https://stackoverflow.com/search?q={query}' },
    { key: 'steam',     name: 'Steam',     iconHost: 'store.steampowered.com/',          url: 'https://store.steampowered.com/search/?term={query}' },
	{ key: 'linkedin',   name: 'LinkedIn',   iconHost: 'linkedin.com',    url: 'https://www.linkedin.com/search/results/all/?keywords={query}' },
	{ key: 'webarchives',   name: 'Web Archives',   iconHost: 'archive.org',    url: 'https://web.archive.org/web/*/{query}' },
    { key: 'rutracker',     name: 'RuTracker',     iconHost: 'rutracker.org',          url: 'https://rutracker.org/forum/tracker.php?nm={query}' },
	{ key: 'kinozal',     name: 'Кинозал.ТВ',     iconHost: 'kinozal.tv',          url: 'https://kinozal.tv/browse.php?s={query}' },
	{ key: 'rutor',     name: 'rutor',     iconHost: 'rutor.info',          url: 'https://rutor.info/search/{query}' },
  ];

  // Helpers for icons
  const buildIconUrl = (host) => `https://${host}/favicon.ico`;
  const buildFallbackIconUrl = (host) => {
    const clean = host.replace(/^www\./, '');
    return `https://icons.duckduckgo.com/ip3/${clean}.ico`;
  };

  // Load engines + enabled keys
  function validateEngines(list) {
    const seen = new Set();
    const valid = [];
    for (const e of Array.isArray(list) ? list : []) {
      if (!e || !e.key || !e.name || !e.iconHost || !e.url) continue;
      if (seen.has(e.key)) continue;
      seen.add(e.key);
      valid.push({ key: String(e.key), name: String(e.name), iconHost: String(e.iconHost), url: String(e.url) });
    }
    return valid.length ? valid : DEFAULT_ENGINES.slice();
  }

  let allEngines = validateEngines(await GM.getValue(STORAGE_KEYS.engines, DEFAULT_ENGINES));
  let enabledEngineKeys = await GM.getValue(
    STORAGE_KEYS.enabledEngineKeys,
    allEngines.map(e => e.key)
  );
  enabledEngineKeys = enabledEngineKeys.filter(k => allEngines.some(e => e.key === k));
  let reuseTabEnabled = await GM.getValue(STORAGE_KEYS.reuseTab, false);
  let showCopyButtonEnabled = await GM.getValue(STORAGE_KEYS.showCopyButton, true);
  let autoCopyOnSelectEnabled = await GM.getValue(STORAGE_KEYS.autoCopyOnSelect, false); // New setting variable

  const getEnabledEngines = () =>
    allEngines.filter(e => enabledEngineKeys.includes(e.key));

  async function copyToClipboard(text) {
    try {
      if (navigator.clipboard && window.isSecureContext) {
        await navigator.clipboard.writeText(text);
        return true;
      }
    } catch {}
    try {
      if (typeof GM_setClipboard === 'function') {
        GM_setClipboard(text);
        return true;
      }
    } catch {}
    try {
      const ta = document.createElement('textarea');
      ta.value = text;
      ta.style.position = 'fixed';
      ta.style.opacity = '0';
      document.body.appendChild(ta);
      ta.focus();
      ta.select();
      const ok = document.execCommand('copy');
      document.body.removeChild(ta);
      return ok;
    } catch {}
    return false;
  }

  // Icon preload
  let iconSrcMap = new Map();
  function waitForImage(url) {
    return new Promise((resolve, reject) => {
      const img = new Image();
      img.referrerPolicy = 'no-referrer';
      img.onload = () => resolve(url);
      img.onerror = () => reject(url);
      img.src = url;
    });
  }
  async function preloadIconForHost(host) {
    const primary = buildIconUrl(host);
    try {
      return await waitForImage(primary);
    } catch {
      const fallback = buildFallbackIconUrl(host);
      try {
        return await waitForImage(fallback);
      } catch {
        return fallback;
      }
    }
  }
  async function preloadIcons(enginesList) {
    await Promise.all(
      enginesList.map(async (engine) => {
        const src = await preloadIconForHost(engine.iconHost);
        iconSrcMap.set(engine.key, src);
      })
    );
  }

  preloadIcons(getEnabledEngines()).catch(() => {});

  // UI helpers
  function createIconImgForEngine(engine) {
    const img = document.createElement('img');
    img.alt = engine.name;
    img.referrerPolicy = 'no-referrer';
    img.decoding = 'async';
    const preloaded = iconSrcMap.get(engine.key);
    img.src = preloaded || buildIconUrl(engine.iconHost);
    img.addEventListener('error', () => {
      if (img.dataset.fallbackApplied) return;
      img.dataset.fallbackApplied = '1';
      img.src = buildFallbackIconUrl(engine.iconHost);
    });
    return img;
  }

  function slugify(str) {
    return String(str || '')
      .toLowerCase()
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/(^-|-$)/g, '') || 'site';
  }
  function generateUniqueKey(base, existingKeys) {
    let key = slugify(base);
    if (!key) key = 'site';
    let out = key;
    let i = 2;
    const set = new Set(existingKeys);
    while (set.has(out)) {
      out = `${key}-${i++}`;
    }
    return out;
  }
  function normalizeHost(input) {
    const s = String(input || '').trim();
    if (!s) return '';
    try {
      const u = /^https?:\/\//i.test(s) ? new URL(s) : new URL(`https://${s}`);
      return u.hostname;
    } catch {
      return s.replace(/^https?:\/\//i, '').split('/')[0];
    }
  }

  async function saveAll(engines, enabledKeys, reuseTab, showCopy, autoCopy) {
    allEngines = engines.slice();
    enabledEngineKeys = enabledKeys.filter(k => allEngines.some(e => e.key === k));
    reuseTabEnabled = !!reuseTab;
    showCopyButtonEnabled = !!showCopy;
    autoCopyOnSelectEnabled = !!autoCopy; // Save new setting state

    await GM.setValue(STORAGE_KEYS.engines, allEngines);
    await GM.setValue(STORAGE_KEYS.enabledEngineKeys, enabledEngineKeys);
    await GM.setValue(STORAGE_KEYS.reuseTab, reuseTabEnabled);
    await GM.setValue(STORAGE_KEYS.showCopyButton, showCopyButtonEnabled);
    await GM.setValue(STORAGE_KEYS.autoCopyOnSelect, autoCopyOnSelectEnabled); // Persist new setting

    iconSrcMap.clear();
    await preloadIcons(allEngines.filter(e => enabledEngineKeys.includes(e.key)));
  }

  GM_addStyle(`
    #${POPUP_ID} {
      --tile: 34px;
      --gap: 6px;
      --maxCols: 5;

      position: absolute;
      z-index: 999999999;
      background-color: rgba(40, 42, 54, 0.95);
      border-radius: 8px;
      box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
      display: flex;
      flex-wrap: wrap;
      align-items: center;
      padding: 6px;
      gap: var(--gap);
      opacity: 0;
      transform: translateY(10px);
      transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;
      backdrop-filter: blur(5px);
      border: 1px solid rgba(255, 255, 255, 0.1);

      width: max-content;
      max-width: calc(var(--tile) * var(--maxCols) + var(--gap) * (var(--maxCols) - 1));
    }
    #${POPUP_ID}.visible { opacity: 1; transform: translateY(0); }

    #${POPUP_ID} a {
      display: flex;
      align-items: center;
      justify-content: center;
      width: var(--tile);
      height: var(--tile);
      background-color: rgba(255, 255, 255, 0.1);
      border-radius: 6px;
      transition: all 0.2s ease;
      cursor: pointer;
      user-select: none;
      flex: 0 0 var(--tile);
    }
    #${POPUP_ID} a:hover {
      background-color: rgba(255, 255, 255, 0.2);
      transform: translateY(-2px);
      box-shadow: 0 2px 5px rgba(0,0,0,0.2);
    }
    #${POPUP_ID} img {
      width: 18px;
      height: 18px;
      object-fit: contain;
      border-radius: 2px;
    }
    #${POPUP_ID} .ssp-settings-btn,
    #${POPUP_ID} .ssp-copy-btn {
      font-size: 18px;
      line-height: 1;
      color: #fff;
      font-family: system-ui, sans-serif;
    }

    /* Settings modal */
    #${SETTINGS_OVERLAY_ID} {
      position: fixed;
      inset: 0;
      z-index: 2147483647;
      background: rgba(0,0,0,0.45);
      display: flex;
      align-items: center;
      justify-content: center;
    }
    #${SETTINGS_MODAL_ID} {
      width: min(780px, 95vw);
      max-height: 85vh;
      overflow: auto;
      background: #1e1f25;
      color: #fff;
      border: 1px solid rgba(255,255,255,0.12);
      border-radius: 10px;
      box-shadow: 0 10px 40px rgba(0,0,0,0.5);
      padding: 16px;
      font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
    }
    #${SETTINGS_MODAL_ID} h2 { margin: 0 0 10px; font-size: 18px; }
    #${SETTINGS_MODAL_ID} .ssp-top {
      display: flex; align-items: center; justify-content: space-between; gap: 8px;
    }
    #${SETTINGS_MODAL_ID} .ssp-muted { opacity: 0.75; }
    #${SETTINGS_MODAL_ID} .ssp-grid {
      display: grid;
      grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
      gap: 10px; margin: 12px 0;
    }
    #${SETTINGS_MODAL_ID} .ssp-item {
      display: flex; align-items: center; justify-content: space-between; gap: 8px;
      padding: 8px; background: rgba(255,255,255,0.07); border-radius: 8px;
    }
    #${SETTINGS_MODAL_ID} .ssp-item label {
      display: flex; align-items: center; gap: 8px; cursor: pointer; width: 100%;
      flex: 1;
    }
    #${SETTINGS_MODAL_ID} .ssp-item img { width: 18px; height: 18px; border-radius: 3px; }
    #${SETTINGS_MODAL_ID} .ssp-row-actions { display: flex; gap: 6px; }
    #${SETTINGS_MODAL_ID} .ssp-icon-btn {
      border: 0; padding: 6px; border-radius: 6px; font-weight: 600; cursor: pointer;
      background: #2e303b; color: #fff; width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
    }
    #${SETTINGS_MODAL_ID} .ssp-icon-btn:hover { background: #3a3d4a; }

    #${SETTINGS_MODAL_ID} .ssp-actions { display: flex; gap: 8px; justify-content: space-between; margin-top: 12px; align-items: flex-end; }
    #${SETTINGS_MODAL_ID} .ssp-actions-right { display: flex; gap: 8px; }
    #${SETTINGS_MODAL_ID} button { border: 0; padding: 8px 12px; border-radius: 8px; font-weight: 600; cursor: pointer; }
    #${SETTINGS_MODAL_ID} .ssp-btn { background: #2e303b; color: #fff; }
    #${SETTINGS_MODAL_ID} .ssp-btn:hover { background: #3a3d4a; }
    #${SETTINGS_MODAL_ID} .ssp-primary { background: #4c7cf3; color: #fff; }
    #${SETTINGS_MODAL_ID} .ssp-primary:hover { background: #3f6be0; }
    #${SETTINGS_MODAL_ID} .ssp-options-label {
        display: flex; align-items: center; gap: 8px; cursor: pointer;
    }
    #${SETTINGS_MODAL_ID} .ssp-btn-group { display: flex; gap: 8px; }
    #${SETTINGS_MODAL_ID} .ssp-actions-left { display: flex; flex-direction: column; gap: 8px; align-items: flex-start; }

    /* Editor overlay inside settings */
    #${SETTINGS_MODAL_ID} .ssp-editor {
      position: fixed; inset: 0; background: rgba(0,0,0,0.5);
      display: none; align-items: center; justify-content: center;
    }
    #${SETTINGS_MODAL_ID} .ssp-editor.visible { display: flex; }
    #${SETTINGS_MODAL_ID} .ssp-editor-card {
      background: #1f212a; color: #fff; border: 1px solid rgba(255,255,255,0.12);
      border-radius: 10px; padding: 14px; width: min(520px, 95vw);
    }
    #${SETTINGS_MODAL_ID} .ssp-form { display: flex; flex-direction: column; gap: 10px; }
    #${SETTINGS_MODAL_ID} .ssp-form label span { display: block; margin-bottom: 4px; opacity: 0.8; font-size: 12px; }
    #${SETTINGS_MODAL_ID} .ssp-form input {
      width: 100%; padding: 8px; border-radius: 8px;
      border: 1px solid rgba(255,255,255,0.15); background: #2b2d38; color: #fff;
    }
    #${SETTINGS_MODAL_ID} .ssp-form .hint { font-size: 12px; opacity: 0.7; }
    #${SETTINGS_MODAL_ID} .ssp-form .editor-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 6px; }
  `);

  let popup = null;
  let searchRequestPending = null;

  function positionPopupAtCursor(popupEl, pageX, pageY) {
    const margin = 10;
    const offsetBelow = 12;
    const offsetAbove = 8;

    const popupW = popupEl.offsetWidth;
    const popupH = popupEl.offsetHeight;

    const viewportW = window.innerWidth;
    const viewportH = window.innerHeight;

    const scrollX = window.pageXOffset;
    const scrollY = window.pageYOffset;

    const clientX = pageX - scrollX;
    const clientY = pageY - scrollY;

    // Horizontal: center, keep inside viewport
    let left = pageX - popupW / 2;
    left = Math.max(scrollX + margin, Math.min(left, scrollX + viewportW - popupW - margin));

    // Vertical: prefer below, else above, else clamp
    const belowTop = pageY + offsetBelow;
    const belowBottomClient = clientY + offsetBelow + popupH;

    let top;
    if (belowBottomClient <= viewportH - margin) {
      top = belowTop;
    } else {
      const aboveTop = pageY - popupH - offsetAbove;
      const aboveTopClient = clientY - popupH - offsetAbove;
      if (aboveTopClient >= margin) {
        top = aboveTop;
      } else {
        top = Math.max(scrollY + margin, Math.min(pageY - popupH / 2, scrollY + viewportH - popupH - margin));
      }
    }

    popupEl.style.left = `${left}px`;
    popupEl.style.top = `${top}px`;
  }

  function createPopup(x, y, selectedText) {
    hidePopup();
    popup = document.createElement('div');
    popup.id = POPUP_ID;

    // Copy
    if (showCopyButtonEnabled) {
      const copyBtn = document.createElement('a');
      copyBtn.className = 'ssp-copy-btn';
      copyBtn.title = 'Copy to clipboard';
      copyBtn.textContent = '📋';
      copyBtn.addEventListener('mousedown', async (e) => {
        e.preventDefault();
        e.stopPropagation();
        await copyToClipboard(selectedText);
        hidePopup();
      });
      popup.appendChild(copyBtn);
    }

    // Engines
    const enginesToShow = getEnabledEngines();
    enginesToShow.forEach(engine => {
      const button = document.createElement('a');
      button.title = `Open in ${engine.name}`;
      const img = createIconImgForEngine(engine);
      button.appendChild(img);

      button.addEventListener('mousedown', (e) => {
        e.preventDefault();
        e.stopPropagation();
        const query = encodeURIComponent(selectedText);
        const searchUrl = engine.url.replace('{query}', query);

        if (reuseTabEnabled) {
          const requestId = Date.now();
          searchRequestPending = requestId;

          GM.setValue(STORAGE_KEYS.searchRequest, {
            url: searchUrl,
            host: engine.iconHost,
            id: requestId,
          });

          setTimeout(() => {
            if (searchRequestPending === requestId) {
              GM_openInTab(searchUrl, { active: true });
              searchRequestPending = null;
            }
          }, 300);

        } else {
          GM_openInTab(searchUrl, { active: true });
        }

        hidePopup();
      });
      popup.appendChild(button);
    });

    // Settings
    const settingsBtn = document.createElement('a');
    settingsBtn.className = 'ssp-settings-btn';
    settingsBtn.title = 'Settings';
    settingsBtn.textContent = '⚙️';
    settingsBtn.addEventListener('mousedown', (e) => {
      e.preventDefault();
      e.stopPropagation();
      hidePopup();
      openSettingsModal();
    });
    popup.appendChild(settingsBtn);

    document.body.appendChild(popup);
    positionPopupAtCursor(popup, x, y);
    setTimeout(() => popup.classList.add('visible'), 10);
  }

  function hidePopup() {
    const existingPopup = document.getElementById(POPUP_ID);
    if (existingPopup) {
      existingPopup.classList.remove('visible');
      setTimeout(() => existingPopup?.remove(), 200);
    }
    popup = null;
  }

  function openSettingsModal() {
    closeSettingsModal();

    const overlay = document.createElement('div');
    overlay.id = SETTINGS_OVERLAY_ID;

    const modal = document.createElement('div');
    modal.id = SETTINGS_MODAL_ID;

    const top = document.createElement('div');
    top.className = 'ssp-top';
    top.innerHTML = `
      <h2>Stylish Search — Settings</h2>
      <div class="ssp-muted">Enable/disable, add or remove sites</div>
    `;

    const actionsHeader = document.createElement('div');
    actionsHeader.className = 'ssp-actions';
    const leftSide = document.createElement('div');
    const rightSide = document.createElement('div');
    rightSide.className = 'ssp-actions-right';

    const btnAddSite = document.createElement('button');
    btnAddSite.className = 'ssp-primary';
    btnAddSite.textContent = 'Add site';

    const btnSelectAll = document.createElement('button');
    btnSelectAll.className = 'ssp-btn';
    btnSelectAll.textContent = 'Select all';

    const btnSelectNone = document.createElement('button');
    btnSelectNone.className = 'ssp-btn';
    btnSelectNone.textContent = 'Clear all';

    rightSide.append(btnAddSite, btnSelectAll, btnSelectNone);
    actionsHeader.append(leftSide, rightSide);

    const grid = document.createElement('div');
    grid.className = 'ssp-grid';

    // Editor overlay (in-modal)
    const editorOverlay = document.createElement('div');
    editorOverlay.className = 'ssp-editor';
    const editorCard = document.createElement('div');
    editorCard.className = 'ssp-editor-card';
    editorOverlay.appendChild(editorCard);

    // Work on copies until Save is clicked
    let workingEngines = allEngines.map(e => ({ ...e }));
    let workingEnabledSet = new Set(enabledEngineKeys);

    function renderGrid() {
      grid.innerHTML = '';
      const currentlyEnabled = workingEnabledSet;

      workingEngines.forEach(engine => {
        const item = document.createElement('div');
        item.className = 'ssp-item';

        const label = document.createElement('label');
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.checked = currentlyEnabled.has(engine.key);
        checkbox.addEventListener('change', () => {
          if (checkbox.checked) currentlyEnabled.add(engine.key);
          else currentlyEnabled.delete(engine.key);
        });

        const img = createIconImgForEngine(engine);
        const span = document.createElement('span');
        span.textContent = engine.name;

        label.append(checkbox, img, span);

        const rowActions = document.createElement('div');
        rowActions.className = 'ssp-row-actions';
        const editBtn = document.createElement('button');
        editBtn.className = 'ssp-icon-btn';
        editBtn.title = 'Edit';
        editBtn.textContent = '✏️';
        editBtn.addEventListener('click', () => openEngineEditor(engine));

        const delBtn = document.createElement('button');
        delBtn.className = 'ssp-icon-btn';
        delBtn.title = 'Delete';
        delBtn.textContent = '🗑️';
        delBtn.addEventListener('click', () => {
          if (!confirm(`Delete "${engine.name}"?`)) return;
          workingEngines = workingEngines.filter(e => e.key !== engine.key);
          currentlyEnabled.delete(engine.key);
          renderGrid();
        });

        rowActions.append(editBtn, delBtn);
        item.append(label, rowActions);
        grid.appendChild(item);
      });
    }

    function openEngineEditor(engine) {
      editorCard.innerHTML = '';
      const isEdit = !!engine;
      const title = document.createElement('h3');
      title.textContent = isEdit ? 'Edit site' : 'Add site';
      title.style.margin = '0 0 8px';

      const form = document.createElement('div');
      form.className = 'ssp-form';

      const nameWrap = document.createElement('label');
      nameWrap.innerHTML = `<span>Name</span>`;
      const nameInput = document.createElement('input');
      nameInput.type = 'text';
      nameInput.placeholder = 'e.g., Stack Overflow';
      nameInput.value = isEdit ? engine.name : '';
      nameWrap.appendChild(nameInput);

      const hostWrap = document.createElement('label');
      hostWrap.innerHTML = `<span>Icon host (used for /favicon.ico)</span>`;
      const hostInput = document.createElement('input');
      hostInput.type = 'text';
      hostInput.placeholder = 'e.g., stackoverflow.com';
      hostInput.value = isEdit ? engine.iconHost : '';
      hostWrap.appendChild(hostInput);

      const urlWrap = document.createElement('label');
      urlWrap.innerHTML = `<span>Search URL (use {query})</span>`;
      const urlInput = document.createElement('input');
      urlInput.type = 'text';
      urlInput.placeholder = 'e.g., https://stackoverflow.com/search?q={query}';
      urlInput.value = isEdit ? engine.url : '';
      urlWrap.appendChild(urlInput);

      const hint = document.createElement('div');
      hint.className = 'hint';
      hint.textContent = 'Example: https://example.com/search?q={query}';

      const editorActions = document.createElement('div');
      editorActions.className = 'editor-actions';

      const cancelBtn = document.createElement('button');
      cancelBtn.className = 'ssp-btn';
      cancelBtn.textContent = 'Cancel';
      cancelBtn.addEventListener('click', () => {
        editorOverlay.classList.remove('visible');
      });

      const saveBtn = document.createElement('button');
      saveBtn.className = 'ssp-primary';
      saveBtn.textContent = 'Save';
      saveBtn.addEventListener('click', async () => {
        const name = nameInput.value.trim();
        const iconHostRaw = hostInput.value.trim();
        const iconHost = normalizeHost(iconHostRaw);
        const url = urlInput.value.trim();

        if (!name) return alert('Name is required');
        if (!iconHost) return alert('Icon host is required');
        if (!url || !url.includes('{query}')) return alert('URL must contain {query} placeholder');

        if (isEdit) {
          const idx = workingEngines.findIndex(e => e.key === engine.key);
          if (idx !== -1) {
            workingEngines[idx] = { ...workingEngines[idx], name, iconHost, url };
          }
          preloadIcons([{ key: engine.key, iconHost }]).catch(() => {});
        } else {
          const key = generateUniqueKey(iconHost || name, workingEngines.map(e => e.key));
          const newEngine = { key, name, iconHost, url };
          workingEngines.push(newEngine);
          workingEnabledSet.add(key);
          preloadIcons([newEngine]).catch(() => {});
        }

        renderGrid();
        editorOverlay.classList.remove('visible');
      });

      editorActions.append(cancelBtn, saveBtn);
      form.append(nameWrap, hostWrap, urlWrap, hint, editorActions);
      editorCard.append(title, form);
      editorOverlay.classList.add('visible');
    }

    // Controls
    btnAddSite.addEventListener('click', () => openEngineEditor(null));
    btnSelectAll.addEventListener('click', () => {
      workingEngines.forEach(e => workingEnabledSet.add(e.key));
      renderGrid();
    });
    btnSelectNone.addEventListener('click', () => {
      workingEnabledSet.clear();
      renderGrid();
    });

    const actionsFooter = document.createElement('div');
    actionsFooter.className = 'ssp-actions';

    const footerLeft = document.createElement('div');
    footerLeft.className = 'ssp-actions-left';

    const optionsWrapper = document.createElement('div');
    optionsWrapper.style.display = 'flex';
    optionsWrapper.style.flexDirection = 'column';
    optionsWrapper.style.gap = '8px';

    const reuseTabLabel = document.createElement('label');
    reuseTabLabel.className = 'ssp-options-label';
    const reuseTabCheckbox = document.createElement('input');
    reuseTabCheckbox.type = 'checkbox';
    reuseTabCheckbox.checked = reuseTabEnabled;
    reuseTabLabel.append(reuseTabCheckbox, 'Open in existing tab (if open)');

    const showCopyLabel = document.createElement('label');
    showCopyLabel.className = 'ssp-options-label';
    const showCopyCheckbox = document.createElement('input');
    showCopyCheckbox.type = 'checkbox';
    showCopyCheckbox.checked = showCopyButtonEnabled;
    showCopyLabel.append(showCopyCheckbox, "Show 'Copy' button");

    const autoCopyOnSelectLabel = document.createElement('label');
    autoCopyOnSelectLabel.className = 'ssp-options-label';
    const autoCopyOnSelectCheckbox = document.createElement('input');
    autoCopyOnSelectCheckbox.type = 'checkbox';
    autoCopyOnSelectCheckbox.checked = autoCopyOnSelectEnabled;
    autoCopyOnSelectLabel.append(autoCopyOnSelectCheckbox, "Auto-copy selected text");

    optionsWrapper.append(reuseTabLabel, showCopyLabel, autoCopyOnSelectLabel);

    const importExportGroup = document.createElement('div');
    importExportGroup.className = 'ssp-btn-group';
    importExportGroup.style.marginTop = '10px';

    const btnImport = document.createElement('button');
    btnImport.className = 'ssp-btn';
    btnImport.textContent = 'Import';
    btnImport.addEventListener('click', handleImport);

    const btnExport = document.createElement('button');
    btnExport.className = 'ssp-btn';
    btnExport.textContent = 'Export';
    btnExport.addEventListener('click', handleExport);

    importExportGroup.append(btnImport, btnExport);
    footerLeft.append(optionsWrapper, importExportGroup);

    function handleExport() {
      const settingsToExport = {
        version: 1,
        engines: allEngines,
        enabledEngineKeys: enabledEngineKeys,
        reuseTab: reuseTabEnabled,
        showCopyButton: showCopyButtonEnabled,
        autoCopyOnSelect: autoCopyOnSelectEnabled,
      };
      const dataStr = JSON.stringify(settingsToExport, null, 2);
      const blob = new Blob([dataStr], {type: "application/json"});
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = 'stylish-search-settings.json';
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);
    }

    function handleImport() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json,application/json';
        input.addEventListener('change', (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);
                    if (!imported || !Array.isArray(imported.engines) || !Array.isArray(imported.enabledEngineKeys)) {
                        throw new Error('Invalid file structure.');
                    }

                    workingEngines = validateEngines(imported.engines);
                    workingEnabledSet = new Set(imported.enabledEngineKeys);
                    reuseTabCheckbox.checked = !!imported.reuseTab;
                    showCopyCheckbox.checked = typeof imported.showCopyButton === 'boolean' ? imported.showCopyButton : true;
                    autoCopyOnSelectCheckbox.checked = typeof imported.autoCopyOnSelect === 'boolean' ? imported.autoCopyOnSelect : false;

                    renderGrid();
                    alert('Settings loaded. Click "Save" to apply the changes.');
                } catch (err) {
                    alert(`Error importing settings: ${err.message}`);
                }
            };
            reader.readAsText(file);
        });
        input.click();
    }


    const footerRight = document.createElement('div');
    footerRight.className = 'ssp-actions-right';

    const btnCancel = document.createElement('button');
    btnCancel.className = 'ssp-btn';
    btnCancel.textContent = 'Cancel';
    btnCancel.addEventListener('click', closeSettingsModal);

    const btnSave = document.createElement('button');
    btnSave.className = 'ssp-primary';
    btnSave.textContent = 'Save';
    btnSave.addEventListener('click', async () => {
      const reuseTab = reuseTabCheckbox.checked;
      const showCopy = showCopyCheckbox.checked;
      const autoCopy = autoCopyOnSelectCheckbox.checked;
      await saveAll(workingEngines, Array.from(workingEnabledSet), reuseTab, showCopy, autoCopy);
      closeSettingsModal();
    });

    footerRight.append(btnCancel, btnSave);
    actionsFooter.append(footerLeft, footerRight);

    modal.append(top, actionsHeader, grid, actionsFooter, editorOverlay);
    overlay.appendChild(modal);

    overlay.addEventListener('mousedown', (e) => {
      if (e.target === overlay) closeSettingsModal();
    });

    document.body.appendChild(overlay);
    renderGrid();
  }

  function closeSettingsModal() {
    const existing = document.getElementById(SETTINGS_OVERLAY_ID);
    if (existing) existing.remove();
  }

  document.addEventListener('mouseup', function (e) {
    if (e.target.closest(`#${POPUP_ID}`)) return;

    setTimeout(() => {
      const selection = window.getSelection();
      const selectedText = selection.toString().trim();

      if (autoCopyOnSelectEnabled && selectedText.length > 0) {
        copyToClipboard(selectedText);
      }

      if (selectedText.length > 2) {
        createPopup(e.pageX, e.pageY, selectedText);
      } else {
        hidePopup();
      }
    }, 50);
  });

  document.addEventListener('mousedown', function (e) {
    if (!e.target.closest(`#${POPUP_ID}`)) hidePopup();
  });

  if (typeof GM_registerMenuCommand === 'function') {
    GM_registerMenuCommand('Stylish Search: Settings', openSettingsModal);
  }

  // Inter-tab communication
  let lastProcessedRequestId = null;
  GM.addValueChangeListener(STORAGE_KEYS.searchRequest, (key, oldValue, newValue, remote) => {
      if (!newValue || !remote || newValue.id === lastProcessedRequestId) {
          return;
      }

      const targetHost = newValue.host;
      const currentHost = location.hostname;

      if (currentHost === targetHost || `www.${currentHost}` === targetHost || currentHost === `www.${targetHost}`) {
          lastProcessedRequestId = newValue.id;
          GM.setValue(STORAGE_KEYS.searchRequest, { ...newValue, handled: true });
          window.location.href = newValue.url;
      }
  });

  GM.addValueChangeListener(STORAGE_KEYS.searchRequest, (key, oldValue, newValue, remote) => {
      if (newValue && newValue.id === searchRequestPending && newValue.handled) {
          searchRequestPending = null;
      }
  });

})();