Quick search by selection + Search engine sidebar
// ==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");
})();