Search Mixer

Quick search by selection + Search engine sidebar

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Search Mixer
// @name:zh-CN   搜索聚合
// @name:zh-TW   搜索聚合
// @license      GPL-3.0 License
// @namespace    https://github.com/Yukari0201/UserScript
// @supportURL   https://github.com/Yukari0201/UserScript
// @homepageURL  https://github.com/Yukari0201/UserScript
// @version      0.1.3
// @description  Quick search by selection + Search engine sidebar
// @description:zh-CN  划词快速搜索 + 搜索引擎侧边栏
// @description:zh-TW  劃詞快速搜索 + 搜索引擎側邊欄
// @author       Yukari0201
// @match        *://*/*
// @require      https://unpkg.com/vue@3/dist/vue.global.prod.js
// @require      data:application/javascript,unsafeWindow.Vue%3DVue%2Cthis.Vue%3DVue%3B
// @require      https://unpkg.com/element-plus
// @require      https://unpkg.com/@element-plus/icons-vue
// @resource     ELEMENT_CSS https://unpkg.com/element-plus/dist/index.css
// @connect      raw.githubusercontent.com
// @connect      jsdelivr.net
// @connect      gh-proxy.org
// @grant        unsafeWindow
// @grant        GM_getResourceText
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @icon         data:image/png;base64,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
// @run-at       document-end
// ==/UserScript==

(function () {
  "use strict";

  // Default config
  const DEFAULT_CONFIG = {
    icon: {
      anchor: "bottom-right", // top-left | top-right | bottom-left | bottom-right
      offsetX: 10,
      offsetY: 10,
    },
    rule: {
      update: true,
      updateInterval: 24 * 60 * 60 * 1000, // 24 hours
      defaultIndex: 0,
    },
    ui: {
      sideBtnPosition: "right", // left | right
      drawerPosition: "rtl", // rtl | ltr | ttb | btt
    },
  };

  let config = GM_getValue("config");
  if (!config || typeof config !== "object") {
    GM_setValue("config", DEFAULT_CONFIG);
    config = DEFAULT_CONFIG;
  }

  // rules URL
  const RULES_URL = [
    "https://raw.githubusercontent.com/Yukari0201/UserScript/refs/heads/main/rules/search-mixer/rules_v1.0.json",
    "https://cdn.jsdelivr.net/gh/Yukari0201/UserScript@refs/heads/main/rules/search-mixer/rules_v1.0.json",
    "https://gcore.jsdelivr.net/gh/Yukari0201/UserScript@refs/heads/main/rules/search-mixer/rules_v1.0.json",
    "https://fastly.jsdelivr.net/gh/Yukari0201/UserScript@refs/heads/main/rules/search-mixer/rules_v1.0.json",
    "https://testingcf.jsdelivr.net/gh/Yukari0201/UserScript@refs/heads/main/rules/search-mixer/rules_v1.0.json",
    "https://gh-proxy.org/https://raw.githubusercontent.com/Yukari0201/UserScript/refs/heads/main/rules/search-mixer/rules_v1.0.json",
    "https://hk.gh-proxy.org/https://raw.githubusercontent.com/Yukari0201/UserScript/refs/heads/main/rules/search-mixer/rules_v1.0.json",
    "https://cdn.gh-proxy.org/https://raw.githubusercontent.com/Yukari0201/UserScript/refs/heads/main/rules/search-mixer/rules_v1.0.json",
    "https://edgeone.gh-proxy.org/https://raw.githubusercontent.com/Yukari0201/UserScript/refs/heads/main/rules/search-mixer/rules_v1.0.json",
  ];

  // Default rules
  const DEFAULT_RULE = {
    lastUpdate: 0,
    rules: [
      {
        name: "Google",
        icon: "https://www.google.com/favicon.ico",
        url: "https://www.google.com/",
        searchPrefix: "https://www.google.com/search?q=",
      },
    ],
  };

  let rule = GM_getValue("rule");
  if (!rule || typeof rule !== "object") {
    GM_setValue("rule", DEFAULT_RULE);
    rule = DEFAULT_RULE;
  }

  const fetchRule = (url) => {
    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest({
        method: "GET",
        url: url,
        timeout: 5000,
        responseType: "json",
        onload: (response) => {
          if (response.status === 200 && response.response) {
            resolve(response.response);
          } else {
            reject(`Status: ${response.status}`);
          }
        },
        onerror: (err) => reject("Network Error"),
        ontimeout: () => reject("Timeout"),
      });
    });
  };

  const updateRules = async () => {
    const now = new Date().getTime();

    // 检查是否开启更新及是否到达间隔时间
    if (
      config.rule.update &&
      now - rule.lastUpdate > config.rule.updateInterval
    ) {
      console.log("Search Mixer: 正在尝试从远程更新规则...");

      for (const url of RULES_URL) {
        try {
          const rulesArr = await fetchRule(url);
          // 更新本地数据
          rule.lastUpdate = new Date().getTime();
          rule.rules = rulesArr;
          GM_setValue("rule", rule);
          console.log(`Search Mixer: 规则更新成功! 来源: ${url}`);
          return; // 成功后立即跳出
        } catch (error) {
          console.warn(`Search Mixer: 尝试从 ${url} 更新失败: ${error}`);
        }
      }
      console.error("Search Mixer: 所有预设的规则地址均无法访问。");
    }
  };

  updateRules();

  // Vue & Element Plus

  const { ElMessageBox } = ElementPlus;
  const elementPlusCss = GM_getResourceText("ELEMENT_CSS");
  GM_addStyle(elementPlusCss);
  GM_addStyle(`
html, body {
  margin: 0;
  padding: 0;
}
#search-mixer-app-container {
  position: fixed;
  ${config.ui.sideBtnPosition}: 10px;
  top: 0;
  height: 100vh;
  display: flex;
  align-items: center;
  z-index: 1000;
  pointer-events: none; /* 使容器不干扰页面的其他元素 */
}
#search-mixer-app {
  pointer-events: auto; /* 恢复应用的可点击性 */
}
.btn-container {
  display: flex;
  flex-direction: column;
  gap: 15px;
}
#search-mixer-app .btn {
  margin-left: 0px !important; /* 去除默认的 margin-left */
  width: 40px !important; /* 固定宽度 */
  height: 40px !important; /* 固定高度 */
  padding: 0 !important; /* 去除默认的 padding */
}
.drawer {
  pointer-events: auto;
}
.search-input-wrapper {
  position: sticky;
  top: 0;
  z-index: 100; 
  background-color: #ffffff; 
  padding: 15px 0; 
  margin-bottom: 10px;
  border-bottom: 1px solid var(--el-border-color-lighter);
}
.search-card-container {
  display: flex;
  flex-direction: column;
  gap: 12px;
}
.search-engine-card {
  cursor: pointer;
  transition: transform 0.2s, box-shadow 0.2s;
  border-radius: 8px !important;
}
.search-engine-card:hover {
  transform: translateX(-4px); /* 悬停时向左轻微位移 */
  border-color: var(--el-color-primary-light-5);
}
.search-card-content {
  display: flex;
  align-items: center;
  gap: 12px;
}
.engine-icon-wrapper {
  width: 32px;
  height: 32px;
  flex-shrink: 0;
}
.engine-icon-wrapper img {
  width: 100%;
  height: 100%;
}
.engine-info {
  display: flex;
  flex-direction: column;
  overflow: hidden; /* 防止 URL 过长溢出 */
}
.engine-name {
  font-size: 14px;
  font-weight: 600;
  line-height: 1.2;
}
.engine-url {
  font-size: 12px;
  color: #999;
  margin-top: 4px;
  /* 强制单行并显示省略号 */
  display: block;
  width: 180px; 
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* 默认引擎卡片的高亮样式 */
.search-engine-card.is-default {
  border: 1.5px solid var(--el-color-primary-light-3) !important;
  background-color: var(--el-color-primary-light-9) !important;
}
/* 调整输入框内选择器的宽度 */
.el-form-item .el-select {
  width: 100%;
}
.quick-search-btn {
  position: fixed;
  z-index: 999999;
  width: 28px;
  height: 28px;
  padding: 4px;
  background: #fff;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.1s;
}
.quick-search-btn:hover {
  transform: scale(1.1);
  background: var(--el-color-primary-light-9);
}
.quick-search-btn img {
  width: 20px;
  height: 20px;
  object-fit: contain;
}
  `);

  let text = `
<div id="search-mixer-app-container">
  <div id="search-mixer-app">
    <div class="btn-container">
      <el-button @click="handleSearchClick" circle type="primary" class="btn">
        <el-icon :size="20"><component is="Search" /></el-icon>
      </el-button>
      <el-button @click="handleSettingClick" circle type="info" class="btn">
        <el-icon :size="20"><component is="Setting" /></el-icon>
      </el-button>
    </div>

    <transition name="el-fade-in">
      <div
        v-if="showQuickBtn"
        class="quick-search-btn"
        :style="quickBtnStyle"
        @mousedown.prevent
        @click="handleQuickSearch"
      >
        <img :src="quickIcon" alt="search" />
      </div>
    </transition>

    <el-drawer 
      v-model="searchDrawer" 
      :direction="direction" 
      size="340px"
      @opened="onDrawerOpened"
    >
      <template #header>
        <div class="drawer-header-content">
          <div class="drawer-title">聚合搜索</div>
          <div class="search-input-wrapper">
            <el-input
              v-model="currentSelection"
              placeholder="请输入搜索内容..."
              clearable
              @keyup.enter="handleEnterSearch"
              ref="searchInput"
            >
              <template #prefix>
                <el-icon><Search /></el-icon>
              </template>
            </el-input>
          </div>
        </div>
      </template>
      
      <template #default>
        <div class="search-card-container">
          <el-card 
            v-for="(item, index) in ruleData.rules" 
            :key="index" 
            shadow="hover" 
            :class="['search-engine-card', index === localConfig.rule.defaultIndex ? 'is-default' : '']"
            @click="openSearch(item)"
          >
            <div class="search-card-content">
              <div class="engine-icon-wrapper">
                <el-image :src="item.icon" fit="contain">
                  <template #error><el-icon><Compass /></el-icon></template>
                </el-image>
              </div>
              <div class="engine-info">
                <div class="engine-name">
                  {{ item.name }}
                  <el-tag v-if="index === localConfig.rule.defaultIndex" size="small" effect="plain" style="margin-left: 8px;">默认</el-tag>
                </div>
                <div class="engine-url">{{ item.url }}</div>
              </div>
            </div>
          </el-card>
        </div>
      </template>
    </el-drawer>

    <el-drawer v-model="settingDrawer" :direction="direction" size="400px" :before-close="handleSettingClose">
      <template #header>
        <h4><el-icon style="vertical-align: middle; margin-right: 8px;"><Setting /></el-icon>搜索聚合设置</h4>
      </template>
      <template #default>
        <el-form :model="localConfig" label-position="top">
          <el-divider content-position="left">界面布局</el-divider>
          <el-form-item label="侧边按钮位置">
            <el-radio-group v-model="localConfig.ui.btnPosition">
              <el-radio-button label="left">左侧</el-radio-button>
              <el-radio-button label="right">右侧</el-radio-button>
            </el-radio-group>
          </el-form-item>
          <el-form-item label="设置面板展开方向">
            <el-select v-model="localConfig.ui.drawerPosition" placeholder="请选择">
              <el-option label="从右往左 (默认)" value="rtl"></el-option>
              <el-option label="从左往右" value="ltr"></el-option>
              <el-option label="从上往下" value="ttb"></el-option>
              <el-option label="从下往上" value="btt"></el-option>
            </el-select>
          </el-form-item>

          <el-divider content-position="left">划词图标 (Icon)</el-divider>
          <el-form-item label="图标锚点位置">
            <el-select v-model="localConfig.icon.anchor">
              <el-option label="左上" value="top-left"></el-option>
              <el-option label="右上" value="top-right"></el-option>
              <el-option label="左下" value="bottom-left"></el-option>
              <el-option label="右下" value="bottom-right"></el-option>
            </el-select>
          </el-form-item>
          <el-row :gutter="20">
            <el-col :span="12">
              <el-form-item label="水平偏移 (px)">
                <el-input-number v-model="localConfig.icon.offsetX" :min="0" :max="100" />
              </el-form-item>
            </el-col>
            <el-col :span="12">
              <el-form-item label="垂直偏移 (px)">
                <el-input-number v-model="localConfig.icon.offsetY" :min="0" :max="100" />
              </el-form-item>
            </el-col>
          </el-row>

          <el-divider content-position="left">规则管理</el-divider>
          <el-form-item label="默认搜索引擎">
            <el-select v-model="localConfig.rule.defaultIndex" placeholder="请选择默认引擎">
              <el-option
                v-for="(item, index) in ruleData.rules"
                :key="index"
                :label="item.name"
                :value="index"
              >
                <template #default>
                  <div style="display: flex; align-items: center; gap: 8px;">
                    <img :src="item.icon" style="width: 16px; height: 16px;" />
                    <span>{{ item.name }}</span>
                  </div>
                </template>
              </el-option>
            </el-select>
          </el-form-item>
          <el-form-item label="自动更新规则">
            <el-switch v-model="localConfig.rule.update" />
          </el-form-item>
          <el-form-item v-if="localConfig.rule.update" label="更新间隔 (小时)">
            <el-input-number 
              :model-value="localConfig.rule.updateInterval / (60 * 60 * 1000)"
              @update:model-value="val => localConfig.rule.updateInterval = val * 60 * 60 * 1000"
              :min="1" :max="720" />
          </el-form-item>
          
          <el-button type="warning" plain @click="resetToDefault" style="width: 100%; margin-top: 20px;">
            恢复默认设置
          </el-button>
        </el-form>
      </template>
      <template #footer>
        <div style="flex: auto">
          <el-button @click="cancelClick">取消</el-button>
          <el-button @click="confirmClick" type="primary">保存配置</el-button>
        </div>
      </template>
    </el-drawer>
  </div>
</div>
  `;

  var div = document.createElement("div");
  div.innerHTML = text;
  document.body.append(div);
  const App = {
    data() {
      return {
        searchDrawer: false,
        settingDrawer: false,
        direction: config.ui.drawerPosition,
        localConfig: JSON.parse(JSON.stringify(config)), // 深拷贝配置对象
        ruleData: rule, // 规则数据
        currentSearchText: "", // 当前搜索框中的文本
        showQuickBtn: false,
        quickBtnStyle: {
          top: "0px",
          left: "0px",
        },
        quickIcon:
          rule.rules[config.rule.defaultIndex].icon ||
          "data:image/png;base64,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",
      };
    },
    mounted() {
      // 全局监听鼠标抬起
      document.addEventListener("mouseup", this.handleMouseUp);
      // 滚动或点击其他地方时隐藏按钮
      document.addEventListener("mousedown", (e) => {
        if (!e.target.closest(".quick-search-btn")) {
          this.showQuickBtn = false;
        }
      });
    },
    methods: {
      handleSearchClick() {
        this.searchDrawer = true;
      },
      handleSearchClose() {
        this.currentSearchText = ""; // 关闭抽屉时清空当前选择
      },
      openSearch(engine) {
        // 判断 currentSearchText 是否非空
        const query = this.currentSearchText
          ? this.currentSearchText.trim()
          : "";

        if (query.length > 0) {
          // 有内容,拼搜索地址
          const searchUrl = engine.searchPrefix + encodeURIComponent(query);
          window.open(searchUrl, "_blank");
        } else {
          // 为空,跳转主页
          window.open(engine.url, "_blank");
        }
      },
      onDrawerOpened() {
        if (this.$refs.searchInput) {
          this.$refs.searchInput.focus();
        }
      },
      handleEnterSearch() {
        const idx = this.localConfig.rule.defaultIndex || 0;
        const defaultEngine = this.ruleData.rules[idx];
        if (defaultEngine) {
          this.openSearch(defaultEngine);
        }
      },
      handleSettingClick() {
        this.settingDrawer = true;
      },
      handleSettingClose(done) {
        ElMessageBox.confirm("Are you sure you want to close?")
          .then(() => {
            done();
          })
          .catch(() => {
            // catch error
          });
      },
      cancelClick() {
        ElMessageBox.confirm("Are you sure you want to close?")
          .then(() => {
            this.settingDrawer = false;
            this.localConfig = JSON.parse(JSON.stringify(config)); // 恢复配置
          })
          .catch(() => {
            // catch error
          });
      },
      confirmClick() {
        // 保存到本地
        GM_setValue("config", this.localConfig);
        ElMessageBox.alert(
          "配置已保存,部分设置可能需要刷新页面生效。",
          "提示",
          {
            confirmButtonText: "确定",
            callback: () => {
              location.reload(); // 或者根据需要手动更新 config 对象
            },
          },
        );
      },
      resetToDefault() {
        ElMessageBox.confirm("确定要恢复默认设置吗?").then(() => {
          this.localConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
        });
      },
      handleMouseUp(e) {
        // 延迟一丢丢,确保 getSelection 能抓到最新的
        setTimeout(() => {
          const selection = window.getSelection();
          const text = selection.toString().trim();

          if (text && text.length > 0) {
            const range = selection.getRangeAt(0);
            const rect = range.getBoundingClientRect();

            // 根据 config 中的锚点逻辑计算位置
            this.calculatePosition(rect);
            this.currentSelection = text;
            this.showQuickBtn = true;
          } else {
            this.showQuickBtn = false;
          }
        }, 10);
      },
      calculatePosition(rect) {
        const { anchor, offsetX, offsetY } = config.icon;
        let top, left;

        // 基础逻辑:以选区矩形为基准
        switch (anchor) {
          case "top-left":
            top = rect.top - 30 - offsetY;
            left = rect.left - offsetX;
            break;
          case "top-right":
            top = rect.top - 30 - offsetY;
            left = rect.right + offsetX;
            break;
          case "bottom-left":
            top = rect.bottom + offsetY;
            left = rect.left - offsetX;
            break;
          case "bottom-right":
          default:
            top = rect.bottom + offsetY;
            left = rect.right + offsetX;
            break;
        }

        this.quickBtnStyle = {
          top: `${top}px`,
          left: `${left}px`,
        };
      },
      handleQuickSearch() {
        const idx = this.localConfig.rule.defaultIndex || 0;
        const engine = this.ruleData.rules[idx];
        if (engine && this.currentSelection) {
          const searchUrl =
            engine.searchPrefix + encodeURIComponent(this.currentSelection);
          window.open(searchUrl, "_blank");
          this.showQuickBtn = false; // 点击后消失
        }
      },
    },
  };

  const app = Vue.createApp(App);
  app.use(ElementPlus);
  for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component);
  }
  app.mount("#search-mixer-app");
})();