// ==UserScript==
// @name 聚合搜索引擎切换导航 + GitHub搜索结果增强(移动端优化)
// @namespace http://tampermonkey.net/
// @version v1.40
// @author 晚风知我意
// @match *://*/*
// @grant unsafeWindow
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect github.com
// @connect raw.githubusercontent.com
// @connect api.github.com
// @icon https://hub.gitmirror.com/https://raw.githubusercontent.com/qq5855144/greasyfork/main/shousuo.svg
// @run-at document-body
// @license MIT
// @description * 搜索引擎快捷工具 + GitHub搜索结果增强 * 核心功能:页面底部搜索引擎快捷栏、GitHub搜索结果显示部署网站和发布版本标签(查询结果受API次数限制,发现不能查询时请开关一次飞行模式,或切换vpn节点)、拖拽排序、自定义引擎管理、快捷搜索
// @downloadURL https://update.greasyfork.icu/scripts/513481/%E8%81%9A%E5%90%88%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E%E5%88%87%E6%8D%A2%E5%AF%BC%E8%88%AA%20%2B%20GitHub%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E5%A2%9E%E5%BC%BA%28%E7%A7%BB%E5%8A%A8%E7%AB%AF%E4%BC%98%E5%8C%96%29.user.js
// @updateURL https://update.greasyfork.icu/scripts/513481/%E8%81%9A%E5%90%88%E6%90%9C%E7%B4%A2%E5%BC%95%E6%93%8E%E5%88%87%E6%8D%A2%E5%AF%BC%E8%88%AA%20%2B%20GitHub%E6%90%9C%E7%B4%A2%E7%BB%93%E6%9E%9C%E5%A2%9E%E5%BC%BA%28%E7%A7%BB%E5%8A%A8%E7%AB%AF%E4%BC%98%E5%8C%96%29.meta.js
// ==/UserScript==
// ===== GitHub 功能模块 =====
const githubEnhancer = {
CONFIG: {
checkInterval: 1000,
maxRetries: 3,
deploymentKeywords: {
'github.io': true,
'vercel.app': true,
'netlify.app': true,
'herokuapp.com': true,
'firebaseapp.com': true,
'pages.dev': true,
'railway.app': true,
'render.com': true,
'surge.sh': true,
'gitlab.io': true
},
excludedExtensions: [
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.svg', '.ico',
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
'.zip', '.rar', '.7z', '.tar', '.gz',
'.md', '.txt', '.json', '.xml', '.yml', '.yaml',
'.js', '.ts', '.jsx', '.tsx', '.css', '.scss', '.less',
'.java', '.py', '.rb', '.php', '.go', '.rs', '.cpp', '.c', '.h',
'.html', '.htm', '.vue', '.svelte',
'.csv', '.tsv', '.sql', '.db',
'.woff', '.woff2', '.ttf', '.eot',
'.mp4', '.avi', '.mov', '.mp3', '.wav', '.flac',
'.log', '.lock', '.env', '.gitignore', '.dockerfile'
]
},
ICONS: {
deployment: ``,
releases: ``
},
processedRepos: new Set(),
init() {
if (this.isGitHubSearchPage()) {
this.injectGitHubStyles();
this.startGitHubProcessing();
this.initGitHubObserver();
}
},
isGitHubSearchPage() {
return window.location.hostname === 'github.com' &&
(window.location.pathname === '/search' ||
window.location.pathname.includes('/search'));
},
injectGitHubStyles() {
const style = document.createElement('style');
style.textContent = `
.github-search-tag {
display: inline-flex;
align-items: center;
padding: 2px 8px;
margin: 2px 4px 2px 0;
font-size: 11px;
font-weight: 500;
border-radius: 12px;
text-decoration: none;
transition: all 0.2s ease;
cursor: pointer;
line-height: 1.4;
white-space: nowrap;
background-color: #DDF4FF !important;
border: none !important;
}
.github-search-tag-deployment {
color: #0284c7 !important;
}
.github-search-tag-releases {
color: #0284c7 !important;
}
.github-search-tag:hover {
opacity: 0.9;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-decoration: none !important;
background-color: #DDF4FF !important;
}
.github-tags-container {
display: flex;
flex-wrap: wrap;
align-items: center;
margin-top: 4px;
gap: 4px;
animation: fadeInUp 0.3s ease-out;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.github-search-tag svg {
display: block;
width: 12px;
height: 12px;
}
`;
document.head.appendChild(style);
},
makeRequest(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: (response) => {
if (response.status === 200) {
resolve(response.responseText);
} else {
reject(new Error(`HTTP ${response.status}`));
}
},
onerror: reject
});
});
},
isValidWebsiteUrl(url) {
try {
const urlObj = new URL(url);
if (!['http:', 'https:'].includes(urlObj.protocol)) {
return false;
}
const pathname = urlObj.pathname.toLowerCase();
const lastSegment = pathname.split('/').pop() || '';
for (const ext of this.CONFIG.excludedExtensions) {
if (lastSegment.endsWith(ext)) {
return false;
}
}
const filePatterns = [
/\/[^/]+\.[a-z0-9]{2,5}$/i,
/\/blob\//,
/\/raw\//,
/\/releases\/download\//,
/\/archive\//,
];
for (const pattern of filePatterns) {
if (pattern.test(url)) {
return false;
}
}
const domain = urlObj.hostname;
for (const keyword in this.CONFIG.deploymentKeywords) {
if (domain.includes(keyword)) {
return true;
}
}
if (domain.includes('github.com') || domain.includes('github.io')) {
return false;
}
return true;
} catch (e) {
return false;
}
},
async checkGitHubPages(repoOwner, repoName) {
try {
const possibleUrls = [
`https://${repoOwner}.github.io`,
`https://${repoOwner}.github.io/${repoName}`,
`https://${repoName}.${repoOwner}.github.io`
];
for (const url of possibleUrls) {
try {
await new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'HEAD',
url: url,
onload: (response) => {
if (response.status < 400) {
resolve(url);
} else {
reject(new Error(`HTTP ${response.status}`));
}
},
onerror: reject
});
});
return url;
} catch (e) {
continue;
}
}
try {
const settingsHtml = await this.makeRequest(`https://github.com/${repoOwner}/${repoName}/settings/pages`);
if (settingsHtml.includes('is published') ||
settingsHtml.includes('is enrolled') ||
settingsHtml.includes('CNAME') ||
settingsHtml.includes('github-pages')) {
const urlMatch = settingsHtml.match(/(https?:\/\/[^\s"']+\.github\.io[^\s"']*)/);
if (urlMatch) {
return urlMatch[0];
}
return `https://${repoOwner}.github.io/${repoName}`;
}
} catch (e) {
// 忽略错误
}
try {
const repoInfo = await this.makeRequest(`https://api.github.com/repos/${repoOwner}/${repoName}`);
const repoData = JSON.parse(repoInfo);
if (repoData.has_pages) {
return `https://${repoOwner}.github.io/${repoName}`;
}
} catch (e) {
// 忽略错误
}
} catch (error) {
// 忽略错误
}
return null;
},
async checkDeployment(repoOwner, repoName) {
try {
const githubPagesUrl = await this.checkGitHubPages(repoOwner, repoName);
if (githubPagesUrl) {
return githubPagesUrl;
}
try {
const readmeUrls = [
`https://raw.githubusercontent.com/${repoOwner}/${repoName}/main/README.md`,
`https://raw.githubusercontent.com/${repoOwner}/${repoName}/master/README.md`,
`https://raw.githubusercontent.com/${repoOwner}/${repoName}/HEAD/README.md`
];
for (const readmeUrl of readmeUrls) {
try {
const readmeText = await this.makeRequest(readmeUrl);
const urlRegex = /(?:https?:\/\/)(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}(?:\/[^\s\)\]>]*)?/g;
const urls = readmeText.match(urlRegex) || [];
const validDeploymentUrls = [];
for (const url of urls) {
if (!this.isValidWebsiteUrl(url)) {
continue;
}
const isKnownPlatform = Object.keys(this.CONFIG.deploymentKeywords).some(keyword =>
url.includes(keyword)
);
if (isKnownPlatform) {
validDeploymentUrls.push(url);
}
}
if (validDeploymentUrls.length > 0) {
return validDeploymentUrls[0];
}
} catch (e) {
continue;
}
}
} catch (e) {
// 忽略错误
}
try {
const packageJsonUrls = [
`https://raw.githubusercontent.com/${repoOwner}/${repoName}/main/package.json`,
`https://raw.githubusercontent.com/${repoOwner}/${repoName}/master/package.json`
];
for (const packageUrl of packageJsonUrls) {
try {
const packageText = await this.makeRequest(packageUrl);
const packageData = JSON.parse(packageText);
if (packageData.homepage &&
typeof packageData.homepage === 'string' &&
packageData.homepage.startsWith('http')) {
const homepage = packageData.homepage;
if (this.isValidWebsiteUrl(homepage)) {
return homepage;
}
}
} catch (e) {
continue;
}
}
} catch (e) {
// 忽略错误
}
} catch (error) {
// 忽略错误
}
return null;
},
async checkReleases(repoOwner, repoName) {
try {
const releasesData = await this.makeRequest(`https://api.github.com/repos/${repoOwner}/${repoName}/releases`);
const releases = JSON.parse(releasesData);
return releases.length > 0;
} catch (e) {
return false;
}
},
createTag(text, href, type) {
const tag = document.createElement('a');
tag.className = `github-search-tag github-search-tag-${type}`;
const iconSpan = document.createElement('span');
iconSpan.innerHTML = type === 'deployment' ? this.ICONS.deployment : this.ICONS.releases;
iconSpan.style.cssText = 'display: inline-flex; align-items: center; margin-right: 4px;';
const textSpan = document.createElement('span');
textSpan.textContent = text;
tag.appendChild(iconSpan);
tag.appendChild(textSpan);
tag.href = href;
tag.target = '_blank';
tag.rel = 'noopener noreferrer';
return tag;
},
findBestPosition(repoItem) {
const positions = [
() => {
const description = repoItem.querySelector('[class*="description"], .jsbtiO');
if (description) {
const container = document.createElement('div');
container.className = 'github-tags-container';
description.parentNode.insertBefore(container, description.nextSibling);
return container;
}
return null;
},
() => {
const metadata = repoItem.querySelector('[class*="metadata"], .dmuROe, .gbntE');
if (metadata) {
const container = document.createElement('div');
container.className = 'github-tags-container';
metadata.appendChild(container);
return container;
}
return null;
},
() => {
const container = document.createElement('div');
container.className = 'github-tags-container';
repoItem.appendChild(container);
return container;
},
() => {
const repoLink = repoItem.querySelector('a[href*="/"][href*="/"]:first-child');
if (repoLink && repoLink.parentNode) {
const container = document.createElement('div');
container.className = 'github-tags-container';
repoLink.parentNode.insertBefore(container, repoLink.nextSibling);
return container;
}
return null;
}
];
for (const positionFinder of positions) {
try {
const container = positionFinder();
if (container) {
return container;
}
} catch (e) {
continue;
}
}
return null;
},
async processRepo(repoItem) {
const repoLink = repoItem.querySelector('a[href*="/"][href*="/"]');
if (!repoLink) return;
const href = repoLink.getAttribute('href');
const match = href.match(/\/([^\/]+)\/([^\/]+)$/);
if (!match) return;
const [_, repoOwner, repoName] = match;
const repoId = `${repoOwner}/${repoName}`;
if (this.processedRepos.has(repoId) || repoItem.dataset.tagsProcessed) {
return;
}
this.processedRepos.add(repoId);
repoItem.dataset.tagsProcessed = 'true';
const tagsContainer = this.findBestPosition(repoItem);
if (!tagsContainer) return;
const loadingTag = document.createElement('span');
loadingTag.innerHTML = '⏳检查中...';
loadingTag.style.cssText = 'font-size: 11px; color: #6a737d; margin-left: 8px; display: inline-flex; align-items: center;';
tagsContainer.appendChild(loadingTag);
try {
const [deploymentUrl, hasReleases] = await Promise.all([
this.checkDeployment(repoOwner, repoName),
this.checkReleases(repoOwner, repoName)
]);
tagsContainer.removeChild(loadingTag);
if (deploymentUrl) {
const deploymentTag = this.createTag('访问网站', deploymentUrl, 'deployment');
tagsContainer.appendChild(deploymentTag);
}
if (hasReleases) {
const releasesUrl = `https://github.com/${repoOwner}/${repoName}/releases`;
const releasesTag = this.createTag('查看版本', releasesUrl, 'releases');
tagsContainer.appendChild(releasesTag);
}
if (tagsContainer.children.length === 0) {
tagsContainer.remove();
}
} catch (error) {
if (loadingTag.parentNode === tagsContainer) {
tagsContainer.removeChild(loadingTag);
}
}
},
findRepoItems() {
const selectors = [
'.fXzjPH',
'[data-testid="repository-card"]',
'.Box-row',
'.repo-list-item'
];
for (const selector of selectors) {
const items = document.querySelectorAll(selector);
if (items.length > 0) {
return Array.from(items);
}
}
const repoLinks = document.querySelectorAll('a[href*="/"][href*="/"]');
return Array.from(repoLinks)
.filter(link => {
const href = link.getAttribute('href');
return href.match(/^\/[^\/]+\/[^\/]+$/);
})
.map(link => link.closest('div, article, li, .Box, .fXzjPH') || link.parentElement)
.filter(item => item && item.querySelector('a[href*="/"][href*="/"]'));
},
processVisibleRepos() {
const repoItems = this.findRepoItems();
repoItems.forEach(repoItem => {
if (!repoItem.dataset.tagsProcessed) {
this.processRepo(repoItem);
}
});
},
startGitHubProcessing() {
let processedCount = 0;
const interval = setInterval(() => {
const currentCount = this.findRepoItems().length;
if (currentCount > 0) {
this.processVisibleRepos();
processedCount++;
if (processedCount >= this.CONFIG.maxRetries ||
this.findRepoItems().every(item => item.dataset.tagsProcessed)) {
clearInterval(interval);
}
}
}, this.CONFIG.checkInterval);
},
initGitHubObserver() {
const observer = new MutationObserver((mutations) => {
let shouldProcess = false;
mutations.forEach((mutation) => {
if (mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
if (node.querySelector && (
node.querySelector('.fXzjPH') ||
node.querySelector('[data-testid="repository-card"]') ||
node.querySelector('a[href*="/"][href*="/"]')
)) {
shouldProcess = true;
}
if (node.matches && (
node.matches('.fXzjPH') ||
node.matches('[data-testid="repository-card"]') ||
(node.querySelector && node.querySelector('a[href*="/"][href*="/"]'))
)) {
shouldProcess = true;
}
}
});
}
});
if (shouldProcess) {
setTimeout(() => this.processVisibleRepos(), 500);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
return observer;
}
};
const punkDeafultMark = "Bing-Google-Baidu-MetaSo-YandexSearch-Bilibili-ApkPure-Quark-Zhihu";
const defaultSearchEngines = [{
name: "谷歌",
searchUrl: "https://www.google.com/search?q={keyword}",
searchkeyName: ["q"],
matchUrl: /google\.com.*?search.*?q=/g,
mark: "Google",
svgCode: `
`
},
{
name: "必应",
searchUrl: "https://www.bing.com/search?q={keyword}",
searchkeyName: ["q"],
matchUrl: /bing\.com.*?search\?q=?/g,
mark: "Bing",
svgCode: `
`
},
{
name: "百度",
searchUrl: "https://www.baidu.com/s?wd={keyword}",
searchkeyName: ["wd", "word"],
matchUrl: /baidu\.com.*?w(or)?d=?/g,
mark: "Baidu",
svgCode: `
`
},
{
name: "密塔",
searchUrl: "https://metaso.cn/?s=itab1&q={keyword}",
searchkeyName: ["q"],
matchUrl: /metaso\.cn.*?q=/g,
mark: "MetaSo",
svgCode: `
`
},
{
name: "Yandex",
searchUrl: "https://yandex.com/search/?text={keyword}",
searchkeyName: ["text"],
matchUrl: /yandex\.com.*?text=/g,
mark: "YandexSearch",
svgCode: `
`
},
{
name: "ApkPure",
searchUrl: "https://apkpure.com/search?q={keyword}",
searchkeyName: ["q"],
matchUrl: /apkpure\.com.*?q=?/g,
mark: "ApkPure",
svgCode: `
`
},
{
name: "哔哩哔哩",
searchUrl: "https://m.bilibili.com/search?keyword={keyword}",
searchkeyName: ["keyword"],
matchUrl: /bilibili\.com.*?keyword=/g,
mark: "Bilibili",
svgCode: `
`
},
{
name: "夸克",
searchUrl: "https://quark.sm.cn/s?q={keyword}",
searchkeyName: ["q"],
matchUrl: /quark\.sm\.cn.*?q=/g,
mark: "Quark",
svgCode: `
`
},
{
name: "扩展搜索",
searchUrl: "https://www.crxsoso.com/search?keyword={keyword}&store=chrome",
searchkeyName: ["keyword"],
matchUrl: /crxsoso\.com\/search\?keyword=/g,
mark: "Crxsoso",
svgCode: `
`
},
{
name: "知乎",
searchUrl: "https://www.zhihu.com/search?type=content&q={keyword}",
searchkeyName: ["q"],
matchUrl: /zhihu\.com.*?q=/g,
mark: "Zhihu",
svgCode: `
`
},
{
name: "GitHub",
searchUrl: "https://github.com/search?q={keyword}+is%3Apublic&type=repositories&s=stars&o=desc",
searchkeyName: ["q"],
matchUrl: /github\.com.*?search\?q=/,
mark: "GitHub",
svgCode: `
`
},
{
name: "YouTube",
searchUrl: "https://www.youtube.com/results?search_query={keyword}",
searchkeyName: ["search_query"],
matchUrl: "youtube\\.com.*?results\\?search_query=",
mark: "YouTube",
svgCode: `
`
},
{
"name": "Baidu图片",
"searchUrl": "https://www.baidu.com/sf/vsearch?pd=image_content&from={source}&atn=page&fr=tab&tn=vsearch&ss=100&sa=tb&rsv_sug4={suggestion}&inputT={input_time}&oq={original_query}&word={keyword}",
"searchkeyName": ["keyword", "source", "suggestion", "input_time", "original_query"],
"matchUrl": /baidu\.com\/sf\/vsearch.*?word=/g,
"mark": "Baidutp",
"svgCode": ` `
},
{
name: "淘宝",
searchUrl: "https://s.taobao.com/search?q={keyword}",
searchkeyName: ["q"],
matchUrl: "taobao\\.com.*?search\\?q=",
mark: "TaoBao",
svgCode: `
`
},
{
name: "PubMed",
searchUrl: "https://pubmed.ncbi.nlm.nih.gov/?term={keyword}",
searchkeyName: ["term"],
matchUrl: "pubmed\\.ncbi\\.nlm\\.nih\\.gov.*?term={keyword}",
mark: "PubMed",
svgCode: `
`
},
{
name: "DuckDuckGo",
searchUrl: "https://duckduckgo.com/?q={keyword}",
searchkeyName: ["q"],
matchUrl: "duckduckgo\\.com.*?q={keyword}",
mark: "DuckDuckGo",
svgCode: `
`
},
{
name: "矢量图库",
searchUrl: "https://www.iconfont.cn/search/index?searchType=icon&q={keyword}",
searchkeyName: ["q"],
matchUrl: /iconfont\.cn\/search\/index\?searchType=icon&q=/g,
mark: "iconfont",
svgCode: `
`
},
{
name: "搜狗",
searchUrl: "https://www.sogou.com/web?query={keyword}",
searchkeyName: ["query"],
matchUrl: /sogou\.com.*?query=/g,
mark: "Sogou",
svgCode: `
`
},
{
name: "猫脚本",
searchUrl: "https://scriptcat.org/zh-CN/search?keyword={keyword}",
searchkeyName: ["keyword"],
matchUrl: /scriptcat\.org\/zh-CN\/search\?keyword=/g,
mark: "ScriptCat",
svgCode: `
`
},
{
name: "360搜索",
searchUrl: "https://www.so.com/s?q={keyword}",
searchkeyName: ["q"],
matchUrl: /so\.com.*?q=/g,
mark: "360Search",
svgCode: `
`
},
{
name: "Startpage",
searchUrl: "https://www.startpage.com/sp/search?query={keyword}",
searchkeyName: ["query"],
matchUrl: /startpage\.com.*?query=/g,
mark: "Startpage",
svgCode: `
`
},
{
name: "WolframAlpha",
searchUrl: "https://www.wolframalpha.com/input?i={keyword}",
searchkeyName: ["i"],
matchUrl: /wolframalpha\.com.*?i=/g,
mark: "WolframAlpha",
svgCode: ``
},
{
name: "谷歌学术",
searchUrl: "https://scholar.google.com/scholar?q={keyword}",
searchkeyName: ["q"],
matchUrl: /scholar\.google\..*?q=/g,
mark: "GoogleScholar",
svgCode: ``
},
{
name: "百度学术",
searchUrl: "https://xueshu.baidu.com/s?wd={keyword}",
searchkeyName: ["wd"],
matchUrl: /xueshu\.baidu\.com.*?wd=/g,
mark: "BaiduScholar",
svgCode: ``
},
{
name: "CNKI",
searchUrl: "https://search.cnki.net/search.aspx?q={keyword}",
searchkeyName: ["q"],
matchUrl: /cnki\.net.*?q=/g,
mark: "CNKI",
svgCode: ``
},
{
name: "StackOverflow",
searchUrl: "https://stackoverflow.com/search?q={keyword}",
searchkeyName: ["q"],
matchUrl: /stackoverflow\.com.*?search\?q=/g,
mark: "StackOverflow",
svgCode: ``
},
{
name: "MDN",
searchUrl: "https://developer.mozilla.org/zh-CN/search?q={keyword}",
searchkeyName: ["q"],
matchUrl: /developer\.mozilla\.org.*?q=/g,
mark: "MDN",
svgCode: ` `
},
{
name: "Coursera",
searchUrl: "https://www.coursera.org/search?query={keyword}",
searchkeyName: ["query"],
matchUrl: /coursera\.org.*?query=/g,
mark: "Coursera",
svgCode: ``
},
{
name: "京东",
searchUrl: "https://search.jd.com/Search?keyword={keyword}",
searchkeyName: ["keyword"],
matchUrl: /jd\.com.*?keyword=/g,
mark: "JD",
svgCode: ``
},
{
name: "亚马逊",
searchUrl: "https://www.amazon.com/s?k={keyword}",
searchkeyName: ["k"],
matchUrl: /amazon\..*?k=/g,
mark: "Amazon",
svgCode: `
`
},
{
name: "AliExpress",
searchUrl: "https://www.aliexpress.com/wholesale?SearchText={keyword}",
searchkeyName: ["SearchText"],
matchUrl: /aliexpress\.com.*?SearchText=/g,
mark: "AliExpress",
svgCode: ``
},
{
name: "微博",
searchUrl: "https://s.weibo.com/weibo?q={keyword}",
searchkeyName: ["q"],
matchUrl: /weibo\.com.*?q=/g,
mark: "Weibo",
svgCode: ``
},
{
name: "抖音",
searchUrl: "https://www.douyin.com/search/{keyword}",
searchkeyName: ["keyword"],
matchUrl: /douyin\.com.*?search/g,
mark: "Douyin",
svgCode: ``
},
{
name: "小红书",
searchUrl: "https://www.xiaohongshu.com/search_result?keyword={keyword}",
searchkeyName: ["keyword"],
matchUrl: /xiaohongshu\.com.*?keyword=/g,
mark: "Xiaohongshu",
svgCode: ``
},
{
name: "豆瓣",
searchUrl: "https://www.douban.com/search?q={keyword}",
searchkeyName: ["q"],
matchUrl: /douban\.com.*?q=/g,
mark: "Douban",
svgCode: ``
},
{
name: "IMDb",
searchUrl: "https://www.imdb.com/find?q={keyword}",
searchkeyName: ["q"],
matchUrl: /imdb\.com.*?q=/g,
mark: "IMDb",
svgCode: ``
},
{
name: "RottenTomatoes",
searchUrl: "https://www.rottentomatoes.com/search?search={keyword}",
searchkeyName: ["search"],
matchUrl: /rottentomatoes\.com.*?search=/g,
mark: "RottenTomatoes",
svgCode: ``
},
{
name: "Steam",
searchUrl: "https://store.steampowered.com/search/?term={keyword}",
searchkeyName: ["term"],
matchUrl: /steampowered\.com.*?term=/g,
mark: "Steam",
svgCode: `
`
},
{
name: "Spotify",
searchUrl: "https://open.spotify.com/search/{keyword}",
searchkeyName: ["q"],
matchUrl: /open\.spotify\.com.*?search/g,
mark: "Spotify",
svgCode: ``
},
{
name: "网易云音乐",
searchUrl: "https://music.163.com/#/search/m/?s={keyword}",
searchkeyName: ["s"],
matchUrl: /music\.163\.com.*?s=/g,
mark: "NeteaseMusic",
svgCode: ``
},
{
name: "Pinterest",
searchUrl: "https://www.pinterest.com/search/pins/?q={keyword}",
searchkeyName: ["q"],
matchUrl: /pinterest\..*?q=/g,
mark: "Pinterest",
svgCode: ``
},
{
name: "Flickr",
searchUrl: "https://www.flickr.com/search/?text={keyword}",
searchkeyName: ["text"],
matchUrl: /flickr\.com.*?text=/g,
mark: "Flickr",
svgCode: ``
},
{
name: "维基百科",
searchUrl: "https://zh.wikipedia.org/w/index.php?search={keyword}",
searchkeyName: ["search"],
matchUrl: /wikipedia\.org.*?search=/g,
mark: "Wikipedia",
svgCode: ``
},
{
name: "ArchWiki",
searchUrl: "https://wiki.archlinux.org/index.php?search={keyword}",
searchkeyName: ["search"],
matchUrl: /archlinux\.org.*?search=/g,
mark: "ArchWiki",
svgCode: ``
},
{
name: "微信读书",
searchUrl: "https://weread.qq.com/web/search/books?keyword={keyword}",
searchkeyName: ["keyword"],
matchUrl: /weread\.qq\.com.*?keyword=/g,
mark: "WeRead",
svgCode: ``
},
{
name: "天眼查",
searchUrl: "https://www.tianyancha.com/search?key={keyword}",
searchkeyName: ["key"],
matchUrl: /tianyancha\.com.*?key=/g,
mark: "Tianyancha",
svgCode: ``
},
{
name: "Ecosia",
searchUrl: "https://www.ecosia.org/search?q={keyword}",
searchkeyName: ["q"],
matchUrl: "ecosia\\.org.*?search\\?q=",
mark: "Ecosia",
svgCode: `
`
},
];
// ===== 常量定义区 =====
const CLASS_NAMES = Object.freeze({
ENGINE_CONTAINER: 'engine-container',
ENGINE_DISPLAY: 'engine-display',
ENGINE_BUTTON: 'engine-button',
HAMBURGER_MENU: 'punkjet-hamburger-menu',
SEARCH_OVERLAY: 'punkjet-search-overlay',
MANAGEMENT_PANEL: 'engine-management-panel',
ENGINE_CARD: 'engine-card',
DRAGGING: 'dragging',
DRAG_OVER: 'drag-over'
});
const STORAGE_KEYS = Object.freeze({
USER_SEARCH_ENGINES: 'userSearchEngines',
PUNK_SETUP_SEARCH: 'punk_setup_search',
LAST_SUCCESSFUL_KEYWORDS: 'last_successful_keywords',
CURRENT_INPUT: 'currentInput',
ENGINE_BAR_OFFSET: 'engineBarOffset'
});
const DEFAULT_CONFIG = {
PUNK_DEFAULT_MARK: 'Bing-Google-Baidu-MetaSo-YandexSearch-Bilibili-ApkPure-Quark-Zhihu',
SEARCH_PARAMS: ['q', 'query', 'search', 'keyword', 'keywords', 'wd', 'key'],
MONITORED_INPUT_SELECTOR: 'input[type="text"], input[type="search"], textarea, input#kw',
CHECK_SCOPE_INTERVAL: 1000,
SHOW_SEARCH_BOX_DELAY: 10000,
SCROLL_TIMEOUT_DURATION: 150,
BAIDU_INPUT_DELAY: 500,
DRAG_SORT_DELAY: 500,
ENGINE_BAR_OFFSET_DEFAULT: 0
};
// ===== 可访问性模块 =====
const accessibility = {
initKeyboardNavigation() {
document.addEventListener('keydown', (e) => {
if (e.altKey && e.key === 's') {
e.preventDefault();
searchOverlay.showSearchOverlay();
}
if (e.key === 'Escape') {
if (appState.searchOverlayVisible) searchOverlay.hideSearchOverlay();
if (appState.hamburgerMenuOpen) hamburgerMenu.hideHamburgerMenu();
const panel = document.getElementById(CLASS_NAMES.MANAGEMENT_PANEL);
if (panel && panel.style.display === 'block') managementPanel.closeManagementPanel();
}
if (e.altKey && e.key === 'm') {
e.preventDefault();
hamburgerMenu.toggleHamburgerMenu();
}
if (e.altKey && e.key === 'e') {
e.preventDefault();
managementPanel.showManagementPanel();
}
});
},
enhanceAriaLabels() {
const buttons = document.querySelectorAll(`.${CLASS_NAMES.ENGINE_BUTTON}`);
buttons.forEach(button => {
const engineName = button.getAttribute('title');
button.setAttribute('aria-label', `使用${engineName}搜索`);
button.setAttribute('role', 'button');
button.setAttribute('tabindex', '0');
});
const hamburgerButton = document.querySelector('.engine-hamburger-button');
if (hamburgerButton) {
hamburgerButton.setAttribute('aria-label', '打开菜单');
hamburgerButton.setAttribute('aria-expanded', 'false');
hamburgerButton.setAttribute('aria-haspopup', 'true');
}
const overlay = document.getElementById(CLASS_NAMES.SEARCH_OVERLAY);
if (overlay) {
const searchInput = overlay.querySelector('input');
if (searchInput) searchInput.setAttribute('aria-label', '搜索关键词或网址');
}
},
updateHamburgerAriaState() {
const hamburgerButton = document.querySelector('.engine-hamburger-button');
if (hamburgerButton) {
hamburgerButton.setAttribute('aria-expanded', appState.hamburgerMenuOpen.toString());
}
},
trapFocus(element) {
const focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length === 0) return;
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
const handleKeyDown = (e) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
};
element.addEventListener('keydown', handleKeyDown);
if (!element._focusTrapHandler) element._focusTrapHandler = handleKeyDown;
setTimeout(() => firstElement.focus(), 100);
},
removeFocusTrap(element) {
if (element._focusTrapHandler) {
element.removeEventListener('keydown', element._focusTrapHandler);
delete element._focusTrapHandler;
}
},
init() {
this.initKeyboardNavigation();
setTimeout(() => this.enhanceAriaLabels(), 1000);
const observer = new MutationObserver(() => this.enhanceAriaLabels());
observer.observe(document.body, { childList: true, subtree: true });
}
};
// ===== 防抖工具模块 =====
const debounceUtils = {
timers: new Map(),
debounce(key, fn, delay = 300, immediate = false) {
if (this.timers.has(key)) clearTimeout(this.timers.get(key));
if (immediate && !this.timers.has(key)) {
fn();
this.timers.set(key, setTimeout(() => this.timers.delete(key), delay));
} else {
const timer = setTimeout(() => {
fn();
this.timers.delete(key);
}, delay);
this.timers.set(key, timer);
}
},
throttle(key, fn, limit = 300) {
if (!this.timers.has(key)) {
fn();
this.timers.set(key, setTimeout(() => this.timers.delete(key), limit));
}
},
cancel(key) {
if (this.timers.has(key)) {
clearTimeout(this.timers.get(key));
this.timers.delete(key);
}
},
clearAll() {
this.timers.forEach((timer) => clearTimeout(timer));
this.timers.clear();
}
};
// ===== 工具函数库 =====
const utils = {
clearAllTimeouts() {
if (appState.scrollTimeout) clearTimeout(appState.scrollTimeout);
if (appState.hideTimeout) clearTimeout(appState.hideTimeout);
debounceUtils.clearAll();
},
isEngineContainerExists() {
return document.querySelector(`.${CLASS_NAMES.ENGINE_CONTAINER}`) !== null;
},
isValidScope() {
return appState.searchUrlMap.some(item => window.location.href.match(item.matchUrl) !== null);
},
recordEngineUsage(mark) {
if (hamburgerMenu.sortMode !== 'smart') return;
const usageCounts = GM_getValue('engine_usage_counts', {});
usageCounts[mark] = (usageCounts[mark] || 0) + 1;
GM_setValue('engine_usage_counts', usageCounts);
},
isValidUrl(string) {
try {
const url = new URL(string);
return url.protocol === 'http:' || url.protocol === 'https:';
} catch (_) {
return false;
}
},
getKeywords() {
try {
const url = new URL(window.location.href);
const searchParams = url.searchParams;
let keywords = '';
for (const param of DEFAULT_CONFIG.SEARCH_PARAMS) {
if (searchParams.has(param)) {
keywords = searchParams.get(param).trim();
if (keywords) break;
}
}
if (!keywords) {
for (const urlItem of appState.searchUrlMap) {
if (window.location.href.match(urlItem.matchUrl) !== null) {
for (const keyItem of urlItem.searchkeyName) {
if (searchParams.has(keyItem)) {
keywords = searchParams.get(keyItem).trim();
if (keywords) break;
}
}
if (keywords) break;
}
}
}
if (keywords) {
localStorage.setItem(STORAGE_KEYS.LAST_SUCCESSFUL_KEYWORDS, keywords);
sessionStorage.setItem(STORAGE_KEYS.LAST_SUCCESSFUL_KEYWORDS, keywords);
} else {
keywords = sessionStorage.getItem(STORAGE_KEYS.LAST_SUCCESSFUL_KEYWORDS) ||
localStorage.getItem(STORAGE_KEYS.LAST_SUCCESSFUL_KEYWORDS) || '';
}
return keywords;
} catch (error) {
console.error("获取关键词失败:", error.message, "当前URL:", window.location.href);
return "";
}
},
getSearchKeywords() {
let keywords = "";
if (appState.searchOverlayVisible) {
const searchInput = document.getElementById("overlay-search-input");
if (searchInput && searchInput.value.trim()) return searchInput.value.trim();
}
const baiduInput = document.querySelector('input#kw, input[name="wd"], input[name="word"]');
if (baiduInput && baiduInput.value.trim()) return baiduInput.value.trim();
const allInputs = document.querySelectorAll(DEFAULT_CONFIG.MONITORED_INPUT_SELECTOR);
for (const input of allInputs) {
const inputVal = input.value.trim();
if (inputVal) {
keywords = inputVal;
break;
}
}
if (!keywords) keywords = this.getKeywords().trim();
if (!keywords) keywords = sessionStorage.getItem(STORAGE_KEYS.CURRENT_INPUT) || "";
return keywords;
},
markUnsavedChanges() {
appState.hasUnsavedChanges = true;
const indicator = document.getElementById("unsaved-indicator");
const saveBtn = document.getElementById("panel-save-btn");
if (indicator) indicator.style.display = "block";
if (saveBtn) {
saveBtn.style.opacity = "1";
saveBtn.style.pointerEvents = "auto";
saveBtn.style.background = "#e67e22";
saveBtn.innerHTML = this.createInlineSVG('save') + ' 保存更改';
const handleHover = function(isEnter) {
this.style.transform = isEnter ? "translateY(-2px)" : "translateY(0)";
this.style.boxShadow = isEnter ? "0 4px 8px rgba(0,0,0,0.2)" : "none";
};
saveBtn.removeEventListener("mouseenter", () => {});
saveBtn.removeEventListener("mouseleave", () => {});
saveBtn.addEventListener("mouseenter", () => handleHover.call(saveBtn, true));
saveBtn.addEventListener("mouseleave", () => handleHover.call(saveBtn, false));
}
},
clearUnsavedChanges() {
appState.hasUnsavedChanges = false;
const indicator = document.getElementById("unsaved-indicator");
const saveBtn = document.getElementById("panel-save-btn");
if (indicator) indicator.style.display = "none";
if (saveBtn) {
saveBtn.style.opacity = "0.7";
saveBtn.style.pointerEvents = "none";
saveBtn.style.background = "#95a5a6";
saveBtn.innerHTML = this.createInlineSVG('save') + ' 保存设置';
setTimeout(() => {
if (!appState.hasUnsavedChanges) {
saveBtn.innerHTML = this.createInlineSVG('check') + ' 已保存';
saveBtn.style.background = "#27ae60";
setTimeout(() => {
if (!appState.hasUnsavedChanges) {
saveBtn.innerHTML = this.createInlineSVG('save') + ' 保存设置';
saveBtn.style.background = "#95a5a6";
}
}, 2000);
}
}, 100);
}
},
updateSelectedCount() {
const checkboxes = document.querySelectorAll(`#engine-management-list input[type="checkbox"]:checked`);
const countElement = document.getElementById("selected-count");
if (countElement) {
countElement.innerHTML = this.createInlineSVG('check-circle') + ` 已选择 ${checkboxes.length} 个引擎`;
}
},
saveButtonOrder() {
const container = document.querySelector(`.${CLASS_NAMES.ENGINE_DISPLAY}`);
if (!container) return;
const buttons = container.querySelectorAll(`.${CLASS_NAMES.ENGINE_BUTTON}`);
const newOrder = Array.from(buttons)
.map(btn => btn.getAttribute('data-mark'))
.filter(mark => mark !== null)
.join('-');
GM_setValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, newOrder);
},
createInlineSVG(iconName, color = 'currentColor') {
const icons = {
search: ``,
cog: ``,
sort: ``,
sog: ``,
save: ``,
check: ``,
'check-circle': ``,
times: ``,
plus: ``,
globe: ``,
undo: ``,
eye: ``,
trash: ``,
list: ``,
magic: ``,
palette: ``,
circle: ``,
'paper-plane': ``,
'info-circle': ``
};
return icons[iconName] || icons['circle'];
},
getEngineBarOffset() {
return GM_getValue(STORAGE_KEYS.ENGINE_BAR_OFFSET, DEFAULT_CONFIG.ENGINE_BAR_OFFSET_DEFAULT);
},
setEngineBarOffset(value) {
GM_setValue(STORAGE_KEYS.ENGINE_BAR_OFFSET, parseInt(value));
}
};
// ===== DOM操作模块 =====
const domHandler = {
injectStyle() {
if (document.querySelector(`style#${CLASS_NAMES.ENGINE_CONTAINER}-style`)) return;
const cssNode = document.createElement("style");
cssNode.id = `${CLASS_NAMES.ENGINE_CONTAINER}-style`;
cssNode.textContent = `
.${CLASS_NAMES.ENGINE_CONTAINER} {
display: flex;
position: fixed;
bottom: 0px;
left: 2%;
width: 96%;
height: 36px;
overflow: hidden;
justify-content: center;
align-items: center;
z-index: 1000;
background-color: rgba(255, 255, 255, 0);
margin-top: 1px;
transition: all 0.3s ease;
transform: translateY(0);
opacity: 1;
overflow-y: hidden;
overflow-x: visible;
}
.${CLASS_NAMES.ENGINE_CONTAINER}.hidden {
transform: translateY(100%);
opacity: 0;
}
.${CLASS_NAMES.ENGINE_DISPLAY} {
display: flex;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
height: 100%;
gap: 0px;
flex-grow: 1;
scrollbar-width: none;
-ms-overflow-style: none;
}
.${CLASS_NAMES.ENGINE_DISPLAY}::-webkit-scrollbar {
display: none;
}
.${CLASS_NAMES.ENGINE_BUTTON} {
width: 55.5px;
height: 32px;
padding: 0;
border: 1px solid #f0f0f0;
border-radius: 8px;
background-color: rgba(255, 255, 255, 1);
color: transparent;
font-size: 14px;
cursor: pointer;
margin: 2px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
backdrop-filter: blur(5px);
box-shadow:
1px 1px 1px rgba(0, 0, 0, 0.1),
0px 0px 0px rgba(255, 255, 255, 0.5),
6px 6px 10px rgba(0, 0, 0, 0.1) inset,
-6px -6px 10px rgba(255, 255, 255, 0) inset;
transition: all 0.3s ease;
flex-shrink: 0;
overflow: hidden;
}
.${CLASS_NAMES.ENGINE_BUTTON}:focus {
border: 2px dashed #2196F3;
background-color: #f0f8ff;
}
.${CLASS_NAMES.ENGINE_BUTTON}.selected {
border: 2px dashed #2196F3;
background-color: #f0f8ff;
}
.${CLASS_NAMES.ENGINE_BUTTON}.${CLASS_NAMES.DRAGGING} {
opacity: 0.5;
transform: rotate(5deg);
}
.${CLASS_NAMES.ENGINE_BUTTON}.${CLASS_NAMES.DRAG_OVER} {
border: 2px dashed #2196F3;
background-color: #f0f8ff;
}
.${CLASS_NAMES.ENGINE_CARD} {
transition: all 0.3s ease;
}
#${CLASS_NAMES.MANAGEMENT_PANEL} {
animation: slideIn 0.3s ease;
}
#${CLASS_NAMES.HAMBURGER_MENU} {
animation: slideInLeft 0.3s ease;
}
#${CLASS_NAMES.SEARCH_OVERLAY} {
animation: fadeIn 0.3s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translate(-50%, -48%); }
to { opacity: 1; transform: translate(-50%, -50%); }
}
@keyframes slideInLeft {
from { opacity: 0; transform: translateX(-10px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
`;
document.head.appendChild(cssNode);
},
monitorInputFields() {
const setupInputMonitoring = (input) => {
if (input.dataset.monitored) return;
input.dataset.monitored = true;
const updateCurrentInput = (event) => {
debounceUtils.debounce('input_monitor', () => {
appState.currentInput = event.target.value.trim();
sessionStorage.setItem(STORAGE_KEYS.CURRENT_INPUT, appState.currentInput);
}, 500);
};
input.addEventListener('input', updateCurrentInput);
input.addEventListener('change', updateCurrentInput);
};
document.querySelectorAll(DEFAULT_CONFIG.MONITORED_INPUT_SELECTOR).forEach(setupInputMonitoring);
const observer = new MutationObserver(() => {
document.querySelectorAll(`${DEFAULT_CONFIG.MONITORED_INPUT_SELECTOR}:not([data-monitored])`).forEach(setupInputMonitoring);
});
observer.observe(document.body, { childList: true, subtree: true });
},
updateSearchBoxPosition() {
const punkJetBox = document.getElementById("punkjet-search-box");
if (!punkJetBox) return;
const offsetValue = utils.getEngineBarOffset();
const shouldOffset = document.activeElement && (
document.activeElement.tagName === 'INPUT' ||
document.activeElement.tagName === 'TEXTAREA'
) && !appState.isInteractingWithEngineBar;
punkJetBox.style.bottom = shouldOffset ? `${offsetValue}px` : '0px';
punkJetBox.style.left = '2%';
punkJetBox.style.width = '96%';
punkJetBox.style.transform = appState.punkJetBoxVisible ? "translateY(0)" : "translateY(100%)";
punkJetBox.style.opacity = appState.punkJetBoxVisible ? "1" : "0";
},
createEngineButton(item) {
const button = document.createElement('button');
button.className = CLASS_NAMES.ENGINE_BUTTON;
button.style.backgroundImage = `url('data:image/svg+xml;utf8,${encodeURIComponent(item.svgCode)}')`;
button.setAttribute("url", item.searchUrl);
button.setAttribute("title", item.name);
button.setAttribute("data-mark", item.mark);
button.innerHTML = '';
const handleMouseEnter = () => {
button.style.backgroundColor = 'rgba(241, 241, 241, 1)';
button.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
};
const handleMouseLeave = () => {
button.style.backgroundColor = 'rgba(240, 240, 244, 1)';
button.style.boxShadow = '1px 1px 1px rgba(0, 0, 0, 0.1), 0px 0px 0px rgba(255, 255, 255, 0.5), 6px 6px 10px rgba(0, 0, 0, 0.1) inset, -6px -6px 10px rgba(255, 255, 255, 0) inset';
};
button.addEventListener('mouseover', handleMouseEnter);
button.addEventListener('mouseout', handleMouseLeave);
button.addEventListener('touchstart', (e) => {
appState.isInteractingWithEngineBar = true;
e.stopPropagation();
}, { passive: true });
button.addEventListener('touchend', (e) => {
setTimeout(() => appState.isInteractingWithEngineBar = false, 150);
e.stopPropagation();
}, { passive: true });
button.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
const url = button.getAttribute("url");
const keywords = utils.getSearchKeywords();
const mark = button.getAttribute('data-mark');
utils.recordEngineUsage(mark);
if (url && keywords) {
const finalUrl = url.replace('{keyword}', encodeURIComponent(keywords));
window.open(finalUrl, '_blank');
if (appState.searchOverlayVisible) searchOverlay.hideSearchOverlay();
} else {
searchOverlay.showSearchOverlay();
}
});
return button;
},
createHamburgerButton() {
const hamburgerButton = document.createElement('button');
hamburgerButton.className = "engine-hamburger-button";
hamburgerButton.innerHTML = utils.createInlineSVG('paper-plane');
hamburgerButton.title = "菜单 (Alt+M)";
hamburgerButton.style.cssText = `
width: 32px;
height: 32px;
border: 1px solid #f0f0f0;
border-radius: 7px;
background-color: rgba(255, 255, 255, 1);
box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.1),
0px 0px 0px rgba(255, 255, 255, 0.5),
6px 6px 10px rgba(0, 0, 0, 0.1) inset,
-6px -6px 10px rgba(255, 255, 255, 0) inset;
cursor: pointer;
margin: 3px;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
font-size: 16px;
color: #999999;
transition: all 0.3s ease;
padding: 0;
outline: none;
`;
hamburgerButton.addEventListener('mouseenter', () => {
hamburgerButton.style.backgroundColor = 'rgba(241, 241, 241, 1)';
hamburgerButton.style.transform = 'translateY(-2px)';
hamburgerButton.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
});
hamburgerButton.addEventListener('mouseleave', () => {
hamburgerButton.style.backgroundColor = 'white';
hamburgerButton.style.transform = 'translateY(0)';
hamburgerButton.style.boxShadow = '1px 1px 1px rgba(0, 0, 0, 0.1), 0px 0px 0px rgba(255, 255, 255, 0.5), 6px 6px 10px rgba(0, 0, 0, 0.1) inset, -6px -6px 10px rgba(255, 255, 255, 0) inset';
});
hamburgerButton.addEventListener('mousedown', (e) => e.preventDefault());
hamburgerButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
hamburgerButton.blur();
appState.hamburgerMenuOpen ? hamburgerMenu.hideHamburgerMenu() : hamburgerMenu.showHamburgerMenu();
});
return hamburgerButton;
},
addSearchBox() {
try {
if (utils.isEngineContainerExists()) return;
const punkJetBox = document.createElement("div");
punkJetBox.id = "punkjet-search-box";
punkJetBox.className = CLASS_NAMES.ENGINE_CONTAINER;
punkJetBox.style.cssText = `
display: flex;
z-index: 9999;
position: fixed;
transition: all 0.3s ease;
`;
this.updateSearchBoxPosition();
const ulList = document.createElement('div');
ulList.className = CLASS_NAMES.ENGINE_DISPLAY;
ulList.style.cssText = `
overflow-x: auto;
overflow-y: hidden;
display: flex;
flex-grow: 1;
`;
const hamburgerButton = this.createHamburgerButton();
punkJetBox.appendChild(hamburgerButton);
const fragment = document.createDocumentFragment();
const showList = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK).split('-');
showList.forEach(showMark => {
const item = appState.searchUrlMap.find(engine => engine.mark === showMark);
if (item) fragment.appendChild(this.createEngineButton(item));
});
ulList.appendChild(fragment);
punkJetBox.appendChild(ulList);
document.body.appendChild(punkJetBox);
appState.containerAdded = true;
this.initScrollListener();
window.addEventListener('resize', () => this.updateSearchBoxPosition());
document.addEventListener('focusin', () => this.updateSearchBoxPosition());
document.addEventListener('focusout', () => this.updateSearchBoxPosition());
document.addEventListener('click', (e) => {
if (!e.target.closest(`#${CLASS_NAMES.HAMBURGER_MENU}`) && !e.target.closest('.engine-hamburger-button')) {
hamburgerMenu.hideHamburgerMenu();
}
});
setTimeout(() => this.enableDragAndSort(), DEFAULT_CONFIG.DRAG_SORT_DELAY);
} catch (error) {
console.error("添加搜索框失败:", error.message);
}
},
enableDragAndSort() {
const container = document.querySelector(`.${CLASS_NAMES.ENGINE_DISPLAY}`);
if (!container) return;
const buttons = container.querySelectorAll(`.${CLASS_NAMES.ENGINE_BUTTON}`);
buttons.forEach(button => {
button.draggable = true;
button.addEventListener('dragstart', (e) => {
button.classList.add(CLASS_NAMES.DRAGGING);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', button.getAttribute('url'));
});
button.addEventListener('dragend', () => {
button.classList.remove(CLASS_NAMES.DRAGGING);
utils.saveButtonOrder();
});
button.addEventListener('dragover', (e) => e.preventDefault());
button.addEventListener('dragenter', (e) => {
e.preventDefault();
button.classList.add(CLASS_NAMES.DRAG_OVER);
});
button.addEventListener('dragleave', () => {
button.classList.remove(CLASS_NAMES.DRAG_OVER);
});
button.addEventListener('drop', (e) => {
e.preventDefault();
button.classList.remove(CLASS_NAMES.DRAG_OVER);
const draggingButton = document.querySelector(`.${CLASS_NAMES.DRAGGING}`);
if (draggingButton && draggingButton !== button) {
const buttonsArray = Array.from(container.querySelectorAll(`.${CLASS_NAMES.ENGINE_BUTTON}`));
const draggedIndex = buttonsArray.indexOf(draggingButton);
const targetIndex = buttonsArray.indexOf(button);
if (draggedIndex < targetIndex) {
container.insertBefore(draggingButton, button.nextSibling);
} else {
container.insertBefore(draggingButton, button);
}
utils.markUnsavedChanges();
}
});
});
},
initScrollListener() {
const passiveOptions = { passive: true };
const handleScroll = () => {
const st = window.pageYOffset || document.documentElement.scrollTop;
const isInteractingWithSearchBar = document.querySelector(`.${CLASS_NAMES.ENGINE_CONTAINER}:hover`) !== null;
if (isInteractingWithSearchBar) return;
utils.clearAllTimeouts();
appState.isScrolling = true;
debounceUtils.debounce('scroll_hide', () => {
if (st > appState.lastScrollTop && st > 50) {
this.hideSearchBox();
} else {
this.showSearchBoxImmediately();
}
appState.lastScrollTop = st <= 0 ? 0 : st;
}, 50);
appState.scrollTimeout = setTimeout(() => {
appState.isScrolling = false;
this.showSearchBoxDelayed();
}, DEFAULT_CONFIG.SCROLL_TIMEOUT_DURATION);
};
const handleTouchStart = (e) => {
const isTouchingEngineBar = e.target.closest(`.${CLASS_NAMES.ENGINE_CONTAINER}`) !== null;
if (isTouchingEngineBar) {
appState.isInteractingWithEngineBar = true;
if (e.target.closest(`.${CLASS_NAMES.ENGINE_BUTTON}`)) {
e.preventDefault();
}
} else {
appState.isInteractingWithEngineBar = false;
}
appState.touchStartY = e.touches[0].clientY;
};
const handleTouchMove = (e) => {
if (appState.isInteractingWithEngineBar) return;
if (appState.touchStartY === null) return;
if (e.target.closest(`.${CLASS_NAMES.ENGINE_CONTAINER}`)) return;
const touchY = e.touches[0].clientY;
const diff = appState.touchStartY - touchY;
debounceUtils.throttle('touch_move', () => {
if (Math.abs(diff) > 10) {
diff > 0 ? this.hideSearchBox() : this.showSearchBoxImmediately();
}
}, 100);
};
const handleTouchEnd = (e) => {
if (appState.isInteractingWithEngineBar) {
setTimeout(() => {
appState.isInteractingWithEngineBar = false;
}, 100);
}
appState.touchStartY = null;
this.showSearchBoxDelayed();
};
const handleWheel = (e) => {
if (e.target.closest(`.${CLASS_NAMES.ENGINE_CONTAINER}`)) return;
setTimeout(() => {
const st = window.pageYOffset || document.documentElement.scrollTop;
if (st > appState.lastScrollTop && st > 50) {
this.hideSearchBox();
} else {
this.showSearchBoxImmediately();
}
appState.lastScrollTop = st <= 0 ? 0 : st;
this.showSearchBoxDelayed();
}, 10);
};
window.addEventListener('scroll', handleScroll, passiveOptions);
window.addEventListener('wheel', handleWheel, passiveOptions);
window.addEventListener('touchstart', handleTouchStart, passiveOptions);
window.addEventListener('touchmove', handleTouchMove, passiveOptions);
window.addEventListener('touchend', handleTouchEnd, passiveOptions);
this.initEngineBarTouchHandling();
document.addEventListener('click', (e) => {
if (e.target.closest(`.${CLASS_NAMES.ENGINE_CONTAINER}`)) return;
if (!e.target.closest(`#${CLASS_NAMES.MANAGEMENT_PANEL}`) &&
!e.target.closest(`.${CLASS_NAMES.ENGINE_CONTAINER}`)) {
this.showSearchBoxImmediately();
}
});
document.addEventListener('focusin', (e) => {
if (e.target.matches('input, textarea')) {
this.showSearchBoxImmediately();
}
});
document.addEventListener('mouseenter', (e) => {
if (e.target.closest(`.${CLASS_NAMES.ENGINE_CONTAINER}`) ||
e.target.closest(`.${CLASS_NAMES.ENGINE_BUTTON}`)) {
this.showSearchBoxImmediately();
}
}, true);
const stopPropagationHandler = (e) => {
if (e.target.closest(`.${CLASS_NAMES.ENGINE_CONTAINER}`)) {
e.stopPropagation();
}
};
document.addEventListener('wheel', stopPropagationHandler, passiveOptions);
document.addEventListener('touchmove', stopPropagationHandler, passiveOptions);
},
initEngineBarTouchHandling() {
const engineContainer = document.querySelector(`.${CLASS_NAMES.ENGINE_CONTAINER}`);
if (!engineContainer) return;
const preventPropagation = (e) => {
e.stopPropagation();
};
const touchEvents = ['touchstart', 'touchmove', 'touchend', 'touchcancel'];
touchEvents.forEach(eventType => {
engineContainer.addEventListener(eventType, preventPropagation, { passive: true });
const buttons = engineContainer.querySelectorAll(`.${CLASS_NAMES.ENGINE_BUTTON}`);
buttons.forEach(button => {
button.addEventListener(eventType, preventPropagation, { passive: true });
});
});
engineContainer.addEventListener('touchstart', (e) => {
if (e.target.closest(`.${CLASS_NAMES.ENGINE_BUTTON}`)) {
appState.isInteractingWithEngineBar = true;
}
}, { passive: true });
engineContainer.addEventListener('touchend', () => {
setTimeout(() => {
appState.isInteractingWithEngineBar = false;
}, 150);
}, { passive: true });
},
showSearchBoxImmediately() {
utils.clearAllTimeouts();
if (!appState.punkJetBoxVisible) {
appState.punkJetBoxVisible = true;
this.updateSearchBoxPosition();
}
},
showSearchBoxDelayed() {
utils.clearAllTimeouts();
appState.hideTimeout = setTimeout(() => {
this.showSearchBoxImmediately();
}, DEFAULT_CONFIG.SHOW_SEARCH_BOX_DELAY);
},
hideSearchBox() {
if (appState.punkJetBoxVisible) {
appState.punkJetBoxVisible = false;
this.updateSearchBoxPosition();
}
},
hideHamburgerMenu() {
hamburgerMenu.hideHamburgerMenu();
},
showHamburgerMenu() {
hamburgerMenu.showHamburgerMenu();
},
toggleHamburgerMenu() {
hamburgerMenu.toggleHamburgerMenu();
}
};
// ===== 搜索遮罩层模块 =====
const searchOverlay = {
createSearchOverlay() {
let overlay = document.getElementById(CLASS_NAMES.SEARCH_OVERLAY);
if (overlay) return overlay;
overlay = document.createElement("div");
overlay.id = CLASS_NAMES.SEARCH_OVERLAY;
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(255, 255, 255, 0.98);
z-index: 9998;
display: none;
flex-direction: column;
backdrop-filter: blur(10px);
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
`;
const scrollContainer = document.createElement("div");
scrollContainer.style.cssText = `
width: 100%;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
padding: 10px 0;
box-sizing: border-box;
`;
const searchContainer = document.createElement("div");
searchContainer.style.cssText = `
width: 95%;
max-width: 900px;
min-height: min-content;
background: linear-gradient(145deg, #f8f9fa, #ffffff);
border-radius: 20px;
padding: 25px 20px;
box-shadow:
0 10px 40px rgba(0, 0, 0, 0.1),
0 2px 10px rgba(0, 0, 0, 0.05);
position: relative;
border: 1px solid rgba(255, 255, 255, 0.5);
margin: 10px auto;
box-sizing: border-box;
`;
const updateSearchContainerStyle = () => {
const isMobile = window.innerWidth <= 768;
if (isMobile) {
searchContainer.style.width = '92%';
searchContainer.style.padding = '20px 15px';
searchContainer.style.borderRadius = '16px';
searchContainer.style.margin = '5px auto';
} else {
searchContainer.style.width = '95%';
searchContainer.style.padding = '25px 20px';
searchContainer.style.borderRadius = '20px';
searchContainer.style.margin = '10px auto';
}
};
updateSearchContainerStyle();
window.addEventListener('resize', updateSearchContainerStyle);
const closeBtn = document.createElement("button");
closeBtn.innerHTML = utils.createInlineSVG('times');
closeBtn.setAttribute('aria-label', '关闭搜索');
closeBtn.style.cssText = `
position: absolute;
top: 16px;
right: 16px;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
border: none;
font-size: 18px;
color: #64748b;
cursor: pointer;
padding: 3px;
border-radius: 50%;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.1),
0 2px 6px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(255, 255, 255, 0.8);
z-index: 1;
backdrop-filter: blur(10px);
`;
closeBtn.addEventListener('mouseenter', () => {
closeBtn.style.background = 'linear-gradient(135deg, #ff4757 0%, #ff3742 100%)';
closeBtn.style.color = 'white';
closeBtn.style.transform = 'scale(1.1) rotate(90deg)';
closeBtn.style.boxShadow = '0 8px 25px rgba(255, 71, 87, 0.4)';
});
closeBtn.addEventListener('mouseleave', () => {
closeBtn.style.background = 'linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%)';
closeBtn.style.color = '#64748b';
closeBtn.style.transform = 'scale(1) rotate(0deg)';
closeBtn.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.05)';
});
closeBtn.addEventListener('click', () => this.hideSearchOverlay());
const title = document.createElement("h2");
title.innerHTML = utils.createInlineSVG('search') + ' 快捷搜索 (Alt+S)';
title.style.cssText = `
margin: 0 0 20px 0;
color: #2c3e50;
text-align: center;
font-size: clamp(18px, 4vw, 24px);
text-shadow: 1px 1px 2px rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
word-break: break-word;
`;
const searchInput = document.createElement("input");
searchInput.type = "text";
searchInput.placeholder = "输入关键词或网址...";
searchInput.id = "overlay-search-input";
searchInput.setAttribute('autocomplete', 'off');
searchInput.setAttribute('autocorrect', 'off');
searchInput.setAttribute('autocapitalize', 'off');
searchInput.setAttribute('spellcheck', 'false');
searchInput.style.cssText = `
width: 100%;
padding: 20px 24px;
box-sizing: border-box;
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
border-radius: 16px;
font-size: 18px;
color: #1e293b;
outline: none;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow:
inset 3px 3px 6px rgba(0, 0, 0, 0.04),
inset -3px -3px 6px rgba(255, 255, 255, 0.8),
0 8px 30px rgba(0, 0, 0, 0.08);
border: 2px solid transparent;
margin-bottom: 28px;
-webkit-appearance: none;
font-weight: 500;
line-height: 1.5;
min-height: 64px;
`;
if (/iPhone|iPad|iPod|Android/i.test(navigator.userAgent)) {
searchInput.style.fontSize = '16px';
searchInput.style.padding = '20px 22px';
searchInput.style.minHeight = '50px';
}
searchInput.addEventListener('focus', () => {
searchInput.style.boxShadow =
'inset 3px 3px 6px rgba(0, 0, 0, 0.06), inset -3px -3px 6px rgba(255, 255, 255, 0.9), 0 12px 40px rgba(99, 102, 241, 0.15)';
searchInput.style.borderColor = 'transparent';
searchInput.style.background = 'linear-gradient(135deg, #ffffff 0%, #fefefe 100%)';
searchInput.style.transform = 'translateY(-2px)';
});
searchInput.addEventListener('blur', () => {
searchInput.style.boxShadow =
'inset 3px 3px 6px rgba(0, 0, 0, 0.04), inset -3px -3px 6px rgba(255, 255, 255, 0.8), 0 8px 30px rgba(0, 0, 0, 0.08)';
searchInput.style.borderColor = 'transparent';
searchInput.style.transform = 'translateY(0)';
});
const navigationSection = document.createElement("div");
navigationSection.style.cssText = `margin-top: 10px;`;
const navTitle = document.createElement("h3");
navTitle.innerHTML = utils.createInlineSVG('globe') + ' 常用网站导航';
navTitle.style.cssText = `
color: #2c3e50;
margin-bottom: 15px;
font-size: clamp(16px, 3.5vw, 18px);
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
border-bottom: 2px solid #3498db;
padding-bottom: 8px;
text-align: center;
flex-wrap: wrap;
`;
navigationSection.appendChild(navTitle);
const categoriesContainer = document.createElement("div");
categoriesContainer.style.cssText = `
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 16px;
margin-top: 10px;
`;
const updateGridLayout = () => {
const width = window.innerWidth;
if (width <= 480) {
categoriesContainer.style.gridTemplateColumns = '1fr';
categoriesContainer.style.gap = '12px';
} else if (width <= 768) {
categoriesContainer.style.gridTemplateColumns = 'repeat(auto-fit, minmax(250px, 1fr))';
categoriesContainer.style.gap = '14px';
} else {
categoriesContainer.style.gridTemplateColumns = 'repeat(auto-fit, minmax(280px, 1fr))';
categoriesContainer.style.gap = '16px';
}
};
updateGridLayout();
window.addEventListener('resize', updateGridLayout);
const websiteCategories = [
{
title: "🔧 逆向论坛区",
sites: [
{ name: "MT论坛", url: "https://bbs.binmt.cc" },
{ name: "吾爱破解", url: "https://www.52pojie.cn" },
{ name: "看雪论坛", url: "https://bbs.pediy.com" },
{ name: "飘云阁", url: "https://www.chinapyg.com" },
{ name: "卡饭论坛", url: "https://www.kafan.cn" },
{ name: "绿盟科技社区", url: "https://www.nsfocus.net" },
{ name: "乌云漏洞平台", url: "https://wooyun.x10sec.org" },
{ name: "渗透测试论坛", url: "https://www.hetianlab.com" },
{ name: "XDA Developers", url: "https://forum.xda-developers.com" },
{ name: "Reddit ReverseEngineering", url: "https://www.reddit.com/r/ReverseEngineering" },
{ name: "CrackWatch", url: "https://www.reddit.com/r/CrackWatch" }
]
},
{
title: "💎 软件资源区",
sites: [
{ name: "GETMODS", url: "https://getmodsapk.com/" },
{ name: "APKdone", url: "https://apkdone.com/" },
{ name: "LITEAPKS", url: "https://liteapks.com/" },
{ name: "APKMODY", url: "https://apkmody.com/" },
{ name: "423Down", url: "https://www.423down.com" },
{ name: "果核剥壳", url: "https://www.ghxi.com" },
{ name: "大眼仔旭", url: "https://www.dayanzai.me" },
{ name: "ZD423", url: "https://www.zdfans.com" },
{ name: "软件缘", url: "https://www.appcgn.com" },
{ name: "小众软件", url: "https://www.appinn.com" },
{ name: "Rutor", url: "http://rutor.info" },
{ name: "RuTracker", url: "https://rutracker.org" }
]
},
{
title: "🤖 AI工具",
sites: [
{ name: "ChatGPT", url: "https://chat.openai.com" },
{ name: "deepseek", url: "https://www.deepseek.com/" },
{ name: "Claude", url: "https://claude.ai" },
{ name: "文心一言", url: "https://yiyan.baidu.com" },
{ name: "豆包", url: "https://www.doubao.com/chat/" },
{ name: "讯飞星火", url: "https://xinghuo.xfyun.cn" },
{ name: "智谱清言", url: "https://chatglm.cn" },
{ name: "Midjourney", url: "https://www.midjourney.com" },
{ name: "Stable Diffusion", url: "https://stability.ai" },
{ name: "Notion AI", url: "https://www.notion.so" }
]
},
{
title: "🎬 影视区",
sites: [
{ name: "网飞猫", url: "https://www.ncat21.com/" },
{ name: "毒舌电影", url: "https://www.ncat21.com/" },
{ name: "诺影导航", url: "https://nuoin.com/" },
{ name: "哔哩哔哩", url: "https://www.bilibili.com" },
{ name: "YouTube", url: "https://www.youtube.com" },
{ name: "Netflix", url: "https://www.netflix.com" },
{ name: "低端影视", url: "https://ddys.tv" },
{ name: "NT动漫", url: "https://ntdm8.com/" },
{ name: "AGE动漫", url: "https://m.agedm.io/#/" },
{ name: "樱花动漫", url: "https://www.yhdm.io" },
{ name: "樱花动漫2", url: "https://www.295yhw.com/" },
{ name: "腾讯视频", url: "https://v.qq.com" },
{ name: "爱奇艺", url: "https://www.iqiyi.com" },
{ name: "芒果TV", url: "https://www.mgtv.com" },
{ name: "1905电影网", url: "https://www.1905.com" }
]
},
{
title: "🛠️ 工具区",
sites: [
{ name: "ProcessOn", url: "https://www.processon.com" },
{ name: "SmallPDF", url: "https://smallpdf.com" },
{ name: "iLovePDF", url: "https://www.ilovepdf.com" },
{ name: "TinyPNG", url: "https://tinypng.com" },
{ name: "RemoveBG", url: "https://www.remove.bg" },
{ name: "Canva", url: "https://www.canva.com" },
{ name: "草料二维码", url: "https://cli.im" },
{ name: "石墨文档", url: "https://shimo.im" },
{ name: "腾讯文档", url: "https://docs.qq.com" },
{ name: "讯飞听见", url: "https://www.iflyrec.com" },
{ name: "格式工厂在线版", url: "https://www.pcgeshi.com" },
{ name: "Figma", url: "https://www.figma.com" },
{ name: "Excalidraw", url: "https://excalidraw.com" },
{ name: "Photopea", url: "https://www.photopea.com" }
]
},
{
title: "📚 学习资源",
sites: [
{ name: "知乎", url: "https://www.zhihu.com" },
{ name: "豆瓣", url: "https://www.douban.com" },
{ name: "慕课网", url: "https://www.imooc.com" },
{ name: "B站学习区", url: "https://www.bilibili.com" },
{ name: "Coursera", url: "https://www.coursera.org" },
{ name: "网易云课堂", url: "https://study.163.com" },
{ name: "腾讯课堂", url: "https://ke.qq.com" },
{ name: "可汗学院", url: "https://www.khanacademy.org" },
{ name: "中国大学MOOC", url: "https://www.icourse163.org" },
{ name: "知乎大学", url: "https://www.zhihu.com/university" },
{ name: "豆包文库", url: "https://www.docin.com" },
{ name: "Library Genesis", url: "http://libgen.is" },
{ name: "Z-Library", url: "https://z-lib.is" },
{ name: "Sci-Hub", url: "https://sci-hub.se" }
]
},
{
title: "🛒 生活购物",
sites: [
{ name: "淘宝", url: "https://www.taobao.com" },
{ name: "京东", url: "https://www.jd.com" },
{ name: "拼多多", url: "https://www.pinduoduo.com" },
{ name: "美团", url: "https://www.meituan.com" },
{ name: "饿了么", url: "https://www.ele.me" },
{ name: "苏宁易购", url: "https://www.suning.com" },
{ name: "唯品会", url: "https://www.vip.com" },
{ name: "闲鱼", url: "https://2.taobao.com" },
{ name: "盒马鲜生", url: "https://www.hemaxiansheng.com" },
{ name: "每日优鲜", url: "https://www.missfresh.cn" },
{ name: "亚马逊", url: "https://www.amazon.cn" },
{ name: "当当网", url: "https://www.dangdang.com" },
{ name: "考拉海购", url: "https://www.kaola.com" }
]
},
{
title: "📰 新闻资讯",
sites: [
{ name: "微博", url: "https://weibo.com" },
{ name: "今日头条", url: "https://www.toutiao.com" },
{ name: "澎湃新闻", url: "https://www.thepaper.cn" },
{ name: "虎嗅", url: "https://www.huxiu.com" },
{ name: "36氪", url: "https://www.36kr.com" },
{ name: "人民日报网", url: "https://www.people.com.cn" },
{ name: "新华网", url: "https://www.xinhuanet.com" },
{ name: "央视新闻", url: "https://news.cctv.com" },
{ name: "财新网", url: "https://www.caixin.com" },
{ name: "第一财经", url: "https://www.yicai.com" },
{ name: "界面新闻", url: "https://www.jiemian.com" },
{ name: "华尔街见闻", url: "https://wallstreetcn.com" },
{ name: "雪球", url: "https://xueqiu.com" }
]
},
{
title: "🎵 音乐娱乐",
sites: [
{ name: "网易云音乐", url: "https://music.163.com" },
{ name: "QQ音乐", url: "https://y.qq.com" },
{ name: "酷狗音乐", url: "https://www.kugou.com" },
{ name: "Spotify", url: "https://open.spotify.com" },
{ name: "喜马拉雅", url: "https://www.ximalaya.com" },
{ name: "酷我音乐", url: "https://www.kuwo.cn" },
{ name: "咪咕音乐", url: "https://music.migu.cn" },
{ name: "荔枝FM", url: "https://www.lizhi.fm" },
{ name: "蜻蜓FM", url: "https://www.qingting.fm" },
{ name: "网易云音乐播客", url: "https://music.163.com/podcast" },
{ name: "Bandcamp(独立音乐)", url: "https://bandcamp.com" },
{ name: "SoundCloud", url: "https://soundcloud.com" },
{ name: "Audius", url: "https://audius.co" }
]
},
{
title: "💻 技术社区",
sites: [
{ name: "V2EX", url: "https://www.v2ex.com" },
{ name: "掘金", url: "https://juejin.cn" },
{ name: "SegmentFault", url: "https://segmentfault.com" },
{ name: "CSDN", url: "https://www.csdn.net" },
{ name: "开源中国", url: "https://www.oschina.net" },
{ name: "GitHub", url: "https://github.com" },
{ name: "GitLab", url: "https://about.gitlab.com" },
{ name: "Stack Overflow", url: "https://stackoverflow.com" },
{ name: "华为开发者联盟", url: "https://developer.huawei.com" },
{ name: "小米开发者平台", url: "https://dev.mi.com" },
{ name: "阿里开发者社区", url: "https://developer.aliyun.com" },
{ name: "腾讯云开发者社区", url: "https://cloud.tencent.com/developer" },
{ name: "字节跳动技术团队", url: "https://techblog.bytedance.com" }
]
},
{
title: "🎮 游戏区",
sites: [
{ name: "Steam", url: "https://store.steampowered.com" },
{ name: "Epic Games", url: "https://www.epicgames.com" },
{ name: "GOG", url: "https://www.gog.com" },
{ name: "3DMGAME", url: "https://www.3dmgame.com" },
{ name: "游民星空", url: "https://www.gamersky.com" },
{ name: "游侠网", url: "https://www.ali213.net" },
{ name: "NGA玩家社区", url: "https://bbs.nga.cn" },
{ name: "TapTap", url: "https://www.taptap.cn" },
{ name: "好游快爆", url: "https://www.3839.com" },
{ name: "itch.io", url: "https://itch.io" },
{ name: "GameJolt", url: "https://gamejolt.com" }
]
},
{
title: "🔐 网络安全",
sites: [
{ name: "FreeBuf", url: "https://www.freebuf.com" },
{ name: "安全客", url: "https://www.anquanke.com" },
{ name: "SecWiki", url: "https://www.sec-wiki.com" },
{ name: "HackerOne", url: "https://www.hackerone.com" },
{ name: "Bugcrowd", url: "https://www.bugcrowd.com" },
{ name: "Exploit Database", url: "https://www.exploit-db.com" },
{ name: "Metasploit", url: "https://www.metasploit.com" },
{ name: "Kali Linux", url: "https://www.kali.org" },
{ name: "OWASP", url: "https://owasp.org" },
{ name: "SANS Institute", url: "https://www.sans.org" }
]
},
{
title: "📱 应用下载",
sites: [
{ name: "Google Play", url: "https://play.google.com" },
{ name: "APKPure", url: "https://apkpure.com" },
{ name: "APKMirror", url: "https://www.apkmirror.com" },
{ name: "F-Droid", url: "https://f-droid.org" },
{ name: "Aptoide", url: "https://www.aptoide.com" },
{ name: "豌豆荚", url: "https://www.wandoujia.com" },
{ name: "应用宝", url: "https://sj.qq.com" },
{ name: "小米应用商店", url: "https://app.mi.com" },
{ name: "华为应用市场", url: "https://appgallery.huawei.com" },
{ name: "酷安", url: "https://www.coolapk.com" }
]
},
{
title: "🌐 开发者工具",
sites: [
{ name: "CodePen", url: "https://codepen.io" },
{ name: "JSFiddle", url: "https://jsfiddle.net" },
{ name: "Replit", url: "https://replit.com" },
{ name: "Glitch", url: "https://glitch.com" },
{ name: "CodeSandbox", url: "https://codesandbox.io" },
{ name: "Postman", url: "https://www.postman.com" },
{ name: "Swagger", url: "https://swagger.io" },
{ name: "JSON Formatter", url: "https://jsonformatter.org" },
{ name: "RegExr", url: "https://regexr.com" },
{ name: "DevDocs", url: "https://devdocs.io" }
]
},
{
title: "🎨 设计资源",
sites: [
{ name: "Dribbble", url: "https://dribbble.com" },
{ name: "Behance", url: "https://www.behance.net" },
{ name: "UI中国", url: "https://www.ui.cn" },
{ name: "站酷", url: "https://www.zcool.com.cn" },
{ name: "花瓣网", url: "https://huaban.com" },
{ name: "Pinterest", url: "https://www.pinterest.com" },
{ name: "Unsplash", url: "https://unsplash.com" },
{ name: "Pexels", url: "https://www.pexels.com" },
{ name: "Iconfont", url: "https://www.iconfont.cn" },
{ name: "Flaticon", url: "https://www.flaticon.com" }
]
},
{
title: "📊 数据资源",
sites: [
{ name: "Kaggle", url: "https://www.kaggle.com" },
{ name: "天池大数据", url: "https://tianchi.aliyun.com" },
{ name: "和鲸社区", url: "https://www.kesci.com" },
{ name: "Data.gov", url: "https://www.data.gov" },
{ name: "Google Dataset", url: "https://datasetsearch.research.google.com" },
{ name: "UCI数据集", url: "https://archive.ics.uci.edu" },
{ name: "国家统计局", url: "https://www.stats.gov.cn" },
{ name: "世界银行数据", url: "https://data.worldbank.org" },
{ name: "GitHub数据集", url: "https://github.com/awesomedata/awesome-public-datasets" }
]
}
];
websiteCategories.forEach(category => {
const categoryElement = document.createElement("div");
categoryElement.style.cssText = `
background: rgba(255, 255, 255, 0.9);
border-radius: 12px;
padding: 16px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.06);
transition: transform 0.2s ease;
break-inside: avoid;
`;
categoryElement.addEventListener('mouseenter', () => {
categoryElement.style.transform = 'translateY(-2px)';
categoryElement.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.12)';
});
categoryElement.addEventListener('mouseleave', () => {
categoryElement.style.transform = 'translateY(0)';
categoryElement.style.boxShadow = '0 2px 12px rgba(0, 0, 0, 0.08)';
});
const categoryTitle = document.createElement("h4");
categoryTitle.textContent = category.title;
categoryTitle.style.cssText = `
margin: 0 0 12px 0;
color: #2c3e50;
font-size: 14px;
font-weight: 600;
border-bottom: 1px solid #ecf0f1;
padding-bottom: 8px;
word-break: break-word;
`;
const sitesContainer = document.createElement("div");
sitesContainer.style.cssText = `
display: flex;
flex-wrap: wrap;
gap: 6px;
`;
category.sites.forEach(site => {
const siteLink = document.createElement("a");
siteLink.textContent = site.name;
siteLink.href = site.url;
siteLink.target = "_blank";
siteLink.rel = "noopener noreferrer";
siteLink.style.cssText = `
display: inline-block;
padding: 6px 10px;
background: linear-gradient(145deg, #f8f9fa, #ffffff);
border: 1px solid #e9ecef;
border-radius: 6px;
text-decoration: none;
color: #495057;
font-size: 12px;
transition: all 0.2s ease;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
flex-shrink: 0;
`;
siteLink.addEventListener('mouseenter', () => {
siteLink.style.background = 'linear-gradient(145deg, #3498db, #2980b9)';
siteLink.style.color = 'white';
siteLink.style.transform = 'translateY(-1px)';
siteLink.style.boxShadow = '0 3px 8px rgba(0, 0, 0, 0.15)';
siteLink.style.borderColor = '#2980b9';
});
siteLink.addEventListener('mouseleave', () => {
siteLink.style.background = 'linear-gradient(145deg, #f8f9fa, #ffffff)';
siteLink.style.color = '#495057';
siteLink.style.transform = 'translateY(0)';
siteLink.style.boxShadow = '0 1px 3px rgba(0, 0, 0, 0.08)';
siteLink.style.borderColor = '#e9ecef';
});
siteLink.addEventListener('touchstart', () => {
siteLink.style.background = 'linear-gradient(145deg, #3498db, #2980b9)';
siteLink.style.color = 'white';
}, { passive: true });
sitesContainer.appendChild(siteLink);
});
categoryElement.appendChild(categoryTitle);
categoryElement.appendChild(sitesContainer);
categoriesContainer.appendChild(categoryElement);
});
navigationSection.appendChild(categoriesContainer);
searchContainer.appendChild(closeBtn);
searchContainer.appendChild(title);
searchContainer.appendChild(searchInput);
searchContainer.appendChild(navigationSection);
scrollContainer.appendChild(searchContainer);
overlay.appendChild(scrollContainer);
searchInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.performOverlaySearch();
}
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
this.hideSearchOverlay();
}
});
searchContainer.addEventListener('click', (e) => {
e.stopPropagation();
});
document.body.appendChild(overlay);
return overlay;
},
showSearchOverlay() {
const overlay = this.createSearchOverlay();
const searchInput = document.getElementById("overlay-search-input");
overlay.style.display = 'flex';
appState.searchOverlayVisible = true;
accessibility.trapFocus(overlay);
setTimeout(() => {
searchInput.focus();
searchInput.select();
}, 100);
domHandler.hideHamburgerMenu();
document.body.style.overflow = 'hidden';
},
hideSearchOverlay() {
const overlay = document.getElementById(CLASS_NAMES.SEARCH_OVERLAY);
if (overlay) {
overlay.style.display = 'none';
appState.searchOverlayVisible = false;
accessibility.removeFocusTrap(overlay);
document.body.style.overflow = '';
}
},
performOverlaySearch() {
const searchInput = document.getElementById("overlay-search-input");
const query = searchInput.value.trim();
if (!query) {
searchInput.focus();
return;
}
if (utils.isValidUrl(query)) {
window.open(query, '_blank');
this.hideSearchOverlay();
return;
}
const showList = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK).split('-');
if (showList.length > 0) {
const firstEngine = appState.searchUrlMap.find(item => item.mark === showList[0]);
if (firstEngine) {
const searchUrl = firstEngine.searchUrl.replace('{keyword}', encodeURIComponent(query));
window.open(searchUrl, '_blank');
this.hideSearchOverlay();
}
}
}
};
// ===== 汉堡菜单模块 =====
const hamburgerMenu = {
sortMode: GM_getValue('engine_sort_mode', 'default'),
createHamburgerMenu() {
let menu = document.getElementById(CLASS_NAMES.HAMBURGER_MENU);
if (menu) return menu;
menu = document.createElement("div");
menu.id = CLASS_NAMES.HAMBURGER_MENU;
menu.style.cssText = `
position: fixed;
bottom: 50px;
left: 20px;
background: rgba(255, 255, 255, 0.95);
border-radius: 15px;
box-shadow: 0 5px 25px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(5px);
z-index: 10001;
display: none;
flex-direction: column;
padding: 10px;
gap: 5px;
min-width: 180px;
border: 1px solid rgba(255, 255, 255, 0.2);
`;
const menuItems = [
{
icon: 'search',
text: '快捷搜索 (Alt+S)',
action: () => searchOverlay.showSearchOverlay()
},
{
icon: 'cog',
text: '引擎管理 (Alt+E)',
action: () => managementPanel.showManagementPanel()
},
{
icon: 'sort',
text: '引擎排序设置',
action: (e) => this.showSortContextMenu(e)
}
];
menuItems.forEach(item => {
const menuItem = document.createElement("button");
menuItem.innerHTML = utils.createInlineSVG(item.icon) + ` ${item.text}`;
menuItem.style.cssText = `
display: flex;
align-items: center;
gap: 10px;
padding: 12px 15px;
border: none;
background: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
color: #2c3e50;
transition: all 0.3s ease;
text-align: left;
outline: none;
`;
menuItem.addEventListener('mouseenter', () => {
menuItem.style.background = 'rgba(52, 152, 219, 0.1)';
});
menuItem.addEventListener('mouseleave', () => {
menuItem.style.background = 'none';
});
menuItem.addEventListener('mousedown', (e) => {
e.preventDefault();
});
menuItem.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
menuItem.blur();
item.action(e);
if (item.icon !== 'sort') {
this.hideHamburgerMenu();
}
});
menu.appendChild(menuItem);
});
const setOffsetButton = document.createElement('button');
setOffsetButton.innerHTML = utils.createInlineSVG('sog') + ' 设置底部偏移';
setOffsetButton.style.cssText = `
display: flex;
align-items: center;
gap: 10px;
padding: 12px 15px;
border: none;
background: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
color: #2c3e50;
transition: all 0.3s ease;
text-align: left;
margin-top: 5px;
outline: none;
`;
setOffsetButton.addEventListener('mouseenter', () => {
setOffsetButton.style.background = 'rgba(52, 152, 219, 0.1)';
});
setOffsetButton.addEventListener('mouseleave', () => {
setOffsetButton.style.background = 'none';
});
setOffsetButton.addEventListener('mousedown', (e) => {
e.preventDefault();
});
setOffsetButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
setOffsetButton.blur();
const currentValue = utils.getEngineBarOffset();
const userValue = prompt(`请输入搜索栏在输入法弹出时的底部偏移(单位px):`, currentValue);
if (userValue !== null && !isNaN(userValue)) {
utils.setEngineBarOffset(userValue);
alert(`偏移值已设置为 ${userValue}px`);
domHandler.updateSearchBoxPosition();
}
this.hideHamburgerMenu();
});
menu.appendChild(setOffsetButton);
document.body.appendChild(menu);
return menu;
},
showSortContextMenu(event) {
this.removeSortContextMenu();
const contextMenu = document.createElement('div');
contextMenu.id = 'sort-context-menu';
contextMenu.style.cssText = `
position: absolute;
top: 0;
left: 160px;
background: white;
border-radius: 8px;
box-shadow: 0 3px 15px rgba(0,0,0,0.2);
padding: 5px 0;
min-width: 150px;
z-index: 10002;
border: 1px solid #eee;
`;
const sortOptions = [
{
text: '默认模式',
mode: 'default',
description: '保持拖拽排序'
},
{
text: '智能排序',
mode: 'smart',
description: '按使用频率自动排列'
},
{
text: '关闭',
mode: 'close',
description: ''
}
];
sortOptions.forEach(option => {
const optionItem = document.createElement('button');
optionItem.style.cssText = `
width: 100%;
text-align: left;
padding: 8px 15px;
border: none;
background: none;
cursor: pointer;
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
`;
if (option.mode !== 'close') {
const checkIcon = document.createElement('span');
checkIcon.innerHTML = this.sortMode === option.mode
? utils.createInlineSVG('check', '#27ae60')
: '';
optionItem.appendChild(checkIcon);
const textContainer = document.createElement('div');
textContainer.style.cssText = `display: flex; flex-direction: column;`;
const mainText = document.createElement('span');
mainText.textContent = option.text;
mainText.style.fontWeight = '500';
const descText = document.createElement('span');
descText.textContent = option.description;
descText.style.fontSize = '11px';
descText.style.color = '#7f8c8d';
textContainer.appendChild(mainText);
textContainer.appendChild(descText);
optionItem.appendChild(textContainer);
} else {
optionItem.textContent = option.text;
optionItem.style.justifyContent = 'center';
optionItem.style.color = '#e74c3c';
optionItem.style.marginTop = '5px';
optionItem.style.borderTop = '1px solid #eee';
}
optionItem.addEventListener('click', () => {
if (option.mode === 'close') {
this.removeSortContextMenu();
return;
}
this.sortMode = option.mode;
GM_setValue('engine_sort_mode', option.mode);
this.applyEngineSort();
this.removeSortContextMenu();
this.showHamburgerMenu();
});
optionItem.addEventListener('mouseenter', () => {
optionItem.style.background = 'rgba(52, 152, 219, 0.1)';
});
optionItem.addEventListener('mouseleave', () => {
optionItem.style.background = 'none';
});
contextMenu.appendChild(optionItem);
});
const hamburgerMenuEl = document.getElementById(CLASS_NAMES.HAMBURGER_MENU);
hamburgerMenuEl.appendChild(contextMenu);
document.addEventListener('click', (e) => this.handleClickOutsideContextMenu(e));
},
removeSortContextMenu() {
const contextMenu = document.getElementById('sort-context-menu');
if (contextMenu) contextMenu.remove();
document.removeEventListener('click', (e) => this.handleClickOutsideContextMenu(e));
},
handleClickOutsideContextMenu(e) {
const contextMenu = document.getElementById('sort-context-menu');
const hamburgerMenuEl = document.getElementById(CLASS_NAMES.HAMBURGER_MENU);
const sortMenuItem = hamburgerMenuEl?.querySelector('button:has(svg[aria-label="sort"])');
if (contextMenu && !contextMenu.contains(e.target) && e.target !== sortMenuItem) {
this.removeSortContextMenu();
}
},
applyEngineSort() {
const engineDisplay = document.querySelector(`.${CLASS_NAMES.ENGINE_DISPLAY}`);
if (!engineDisplay) return;
const buttons = Array.from(engineDisplay.querySelectorAll(`.${CLASS_NAMES.ENGINE_BUTTON}`));
if (buttons.length === 0) return;
engineDisplay.innerHTML = '';
if (this.sortMode === 'default') {
const originalOrder = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK).split('-');
const sortedButtons = originalOrder.map(mark =>
buttons.find(btn => btn.getAttribute('data-mark') === mark)
).filter(btn => btn);
sortedButtons.forEach(btn => engineDisplay.appendChild(btn));
domHandler.enableDragAndSort();
} else if (this.sortMode === 'smart') {
const usageCounts = GM_getValue('engine_usage_counts', {});
const sortedButtons = [...buttons].sort((a, b) => {
const aMark = a.getAttribute('data-mark');
const bMark = b.getAttribute('data-mark');
const aCount = usageCounts[aMark] || 0;
const bCount = usageCounts[bMark] || 0;
return bCount - aCount;
});
sortedButtons.forEach(btn => engineDisplay.appendChild(btn));
buttons.forEach(btn => {
btn.draggable = false;
btn.style.cursor = 'default';
});
}
},
showHamburgerMenu() {
const menu = this.createHamburgerMenu();
menu.style.display = 'flex';
appState.hamburgerMenuOpen = true;
accessibility.updateHamburgerAriaState();
accessibility.trapFocus(menu);
},
hideHamburgerMenu() {
const menu = document.getElementById(CLASS_NAMES.HAMBURGER_MENU);
if (menu) {
menu.style.display = 'none';
appState.hamburgerMenuOpen = false;
accessibility.updateHamburgerAriaState();
accessibility.removeFocusTrap(menu);
this.removeSortContextMenu();
}
},
toggleHamburgerMenu() {
appState.hamburgerMenuOpen ?
this.hideHamburgerMenu() :
this.showHamburgerMenu();
}
};
// ===== 管理面板模块 =====
const managementPanel = {
createActionButton(html, color, title) {
const button = document.createElement("button");
button.innerHTML = html;
button.title = title;
button.style.cssText = `
padding: 10px 15px;
background-color: ${color};
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
min-width: 120px;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 5px;
justify-content: center;
`;
button.addEventListener("mouseenter", () => {
button.style.transform = "translateY(-2px)";
button.style.boxShadow = "0 4px 8px rgba(0,0,0,0.2)";
});
button.addEventListener("mouseleave", () => {
button.style.transform = "translateY(0)";
button.style.boxShadow = "none";
});
return button;
},
extractSearchEngineFromPage() {
const searchInfo = { name: "", searchUrl: "", searchkeyName: [], matchUrl: "", mark: "", found: false };
try {
const formResult = this.extractFromSearchForms();
if (formResult.found) return { ...searchInfo, ...formResult };
const inputResult = this.extractFromSearchInputs();
if (inputResult.found) return { ...searchInfo, ...inputResult };
const metaResult = this.extractFromMetaTags();
if (metaResult.found) return { ...searchInfo, ...metaResult };
const urlResult = this.extractFromURLParameters();
if (urlResult.found) return { ...searchInfo, ...urlResult };
const commonResult = this.extractFromCommonPatterns();
if (commonResult.found) return { ...searchInfo, ...commonResult };
} catch (error) {
console.warn('搜索引擎信息提取失败:', error);
}
return searchInfo;
},
extractFromSearchForms() {
const searchForms = document.querySelectorAll('form');
const result = { found: false };
for (const form of searchForms) {
const action = form.getAttribute('action') || '';
const method = (form.getAttribute('method') || 'get').toLowerCase();
const isSearchForm = this.isSearchForm(form, action);
if (!isSearchForm) continue;
const baseUrl = action.startsWith('http') ? action : new URL(action, window.location.origin).href;
const keyParams = this.extractKeyParamsFromForm(form);
if (keyParams.length === 0) continue;
const searchUrl = this.buildSearchUrl(baseUrl, method, keyParams);
const domain = new URL(baseUrl).hostname;
const engineInfo = this.generateEngineInfo(domain, keyParams, searchUrl);
return { ...engineInfo, found: true };
}
return result;
},
extractFromSearchInputs() {
const searchInputSelectors = [
'input[type="search"]', 'input[name*="search"]', 'input[name*="query"]', 'input[name*="q"]',
'input[name*="keyword"]', 'input[name*="key"]', 'input[name*="wd"]', 'input[name*="kw"]',
'input[placeholder*="搜索"]', 'input[placeholder*="search"]', 'input[placeholder*="查询"]',
'input[aria-label*="搜索"]', 'input[aria-label*="search"]'
];
const searchInputs = document.querySelectorAll(searchInputSelectors.join(','));
const result = { found: false };
if (searchInputs.length > 0) {
const input = searchInputs[0];
const name = input.getAttribute('name') || 'q';
const domain = window.location.hostname;
let searchUrl = '';
const form = input.form;
if (form && form.action) {
const baseUrl = form.action.startsWith('http') ? form.action : new URL(form.action, window.location.origin).href;
const method = (form.getAttribute('method') || 'get').toLowerCase();
searchUrl = this.buildSearchUrl(baseUrl, method, [name]);
} else {
searchUrl = `${window.location.origin}/search?${name}={keyword}`;
}
const engineInfo = this.generateEngineInfo(domain, [name], searchUrl);
return { ...engineInfo, found: true };
}
return result;
},
extractFromMetaTags() {
const result = { found: false };
const ogSiteName = document.querySelector('meta[property="og:site_name"]');
const applicationName = document.querySelector('meta[name="application-name"]');
if (ogSiteName || applicationName) {
const siteName = (ogSiteName?.getAttribute('content') || applicationName?.getAttribute('content') || '').toLowerCase();
const knownEngines = ['google', 'bing', 'baidu', 'duckduckgo', 'yahoo', 'yandex'];
const isKnownEngine = knownEngines.some(engine => siteName.includes(engine));
if (isKnownEngine) {
const domain = window.location.hostname;
const keyParams = this.guessKeyParameters();
const searchUrl = `${window.location.origin}/search?${keyParams[0]}={keyword}`;
const engineInfo = this.generateEngineInfo(domain, keyParams, searchUrl);
return { ...engineInfo, found: true };
}
}
return result;
},
extractFromURLParameters() {
const result = { found: false };
const urlParams = new URLSearchParams(window.location.search);
const searchParams = [
'q', 'query', 'search', 'keyword', 'keywords', 'searchword', 'searchquery', 'searchterm',
'searchtext', 'searchkey', 'key', 'wd', 'kw', 'p', 's', 'string', 'phrase', 'terms', 'ask'
];
for (const param of searchParams) {
if (urlParams.has(param)) {
const domain = window.location.hostname;
const searchUrl = `${window.location.origin}${window.location.pathname}?${param}={keyword}`;
const engineInfo = this.generateEngineInfo(domain, [param], searchUrl);
return { ...engineInfo, found: true };
}
}
return result;
},
extractFromCommonPatterns() {
const result = { found: false };
const domain = window.location.hostname;
const knownPatterns = {
'google': { key: 'q', path: '/search' },
'bing': { key: 'q', path: '/search' },
'baidu': { key: 'wd', path: '/s' },
'duckduckgo': { key: 'q', path: '/' },
'yahoo': { key: 'p', path: '/search' },
'yandex': { key: 'text', path: '/search' },
'github': { key: 'q', path: '/search' }
};
for (const [engine, pattern] of Object.entries(knownPatterns)) {
if (domain.includes(engine)) {
const searchUrl = `${window.location.origin}${pattern.path}?${pattern.key}={keyword}`;
const engineInfo = this.generateEngineInfo(domain, [pattern.key], searchUrl);
return { ...engineInfo, found: true };
}
}
return result;
},
isSearchForm(form, action) {
const formHtml = form.outerHTML.toLowerCase();
const actionLower = action.toLowerCase();
const searchIndicators = ['search', 'query', 'find', 'seek', 'lookup', 'q='];
if (searchIndicators.some(indicator => actionLower.includes(indicator) || formHtml.includes(indicator))) {
return true;
}
const inputs = form.querySelectorAll('input[type="text"], input[type="search"]');
for (const input of inputs) {
const name = (input.getAttribute('name') || '').toLowerCase();
const placeholder = (input.getAttribute('placeholder') || '').toLowerCase();
if (searchIndicators.some(indicator => name.includes(indicator) || placeholder.includes(indicator))) {
return true;
}
}
return false;
},
extractKeyParamsFromForm(form) {
const keyParams = [];
const inputs = form.querySelectorAll('input[name]');
const searchParamPatterns = [
/^q$/, /^query/, /^search/, /^keyword/, /^key/, /^wd$/, /^kw$/,
/^string/, /^phrase/, /^terms/, /^ask/, /^find/, /^seek/
];
for (const input of inputs) {
const name = input.getAttribute('name');
if (!name) continue;
const isSearchParam = searchParamPatterns.some(pattern => pattern.test(name));
if (isSearchParam) keyParams.push(name);
}
if (keyParams.length === 0 && inputs.length > 0) {
const firstName = inputs[0].getAttribute('name');
if (firstName) keyParams.push(firstName);
}
return keyParams;
},
buildSearchUrl(baseUrl, method, keyParams) {
if (method === 'post') {
return `${baseUrl}?${keyParams[0]}={keyword}`;
} else {
const separator = baseUrl.includes('?') ? '&' : '?';
return `${baseUrl}${separator}${keyParams[0]}={keyword}`;
}
},
generateEngineInfo(domain, keyParams, searchUrl) {
const cleanDomain = domain.replace('www.', '');
const name = cleanDomain.split('.')[0].charAt(0).toUpperCase() + cleanDomain.split('.')[0].slice(1);
const mark = cleanDomain.replace(/\./g, '_');
return {
name: name,
searchUrl: searchUrl,
searchkeyName: keyParams,
matchUrl: `.*${cleanDomain}.*`,
mark: mark
};
},
guessKeyParameters() {
const commonParams = ['q', 'query', 'search', 'keyword', 'key', 'wd', 'kw'];
return commonParams.slice(0, 1);
},
extractFromCurrentPage() {
const searchInfo = this.extractSearchEngineFromPage();
if (!searchInfo.found) {
alert("无法自动识别当前页面的搜索引擎,请手动添加。");
return;
}
this.showAddForm(true);
document.getElementById("engine-name").value = searchInfo.name;
document.getElementById("engine-mark").value = searchInfo.mark;
document.getElementById("engine-url").value = searchInfo.searchUrl;
document.getElementById("engine-keys").value = searchInfo.searchkeyName.join(",");
const favicon = document.querySelector('link[rel*="icon"]');
if (favicon) {
const iconUrl = favicon.href;
if (!iconUrl.startsWith('data:')) {
document.getElementById("icon-type").value = "image";
document.getElementById("icon-input").value = iconUrl;
this.previewIcon();
}
}
alert(`✅ 已自动识别 ${searchInfo.name} 搜索引擎!请检查并保存。`);
},
showAddForm(show) {
const formSection = document.getElementById("add-engine-form");
const engineList = document.getElementById("engine-management-list");
const listTitle = formSection?.previousElementSibling;
if (!formSection || !engineList || !listTitle) return;
if (show) {
formSection.style.display = "block";
engineList.style.display = "none";
listTitle.style.display = "none";
document.getElementById("engine-name").value = "";
document.getElementById("engine-mark").value = "";
document.getElementById("engine-url").value = "";
document.getElementById("engine-keys").value = "";
document.getElementById("icon-input").value = "";
document.getElementById("icon-preview").innerHTML = "";
} else {
formSection.style.display = "none";
engineList.style.display = "grid";
listTitle.style.display = "block";
}
},
previewIcon() {
const type = document.getElementById("icon-type").value;
const value = document.getElementById("icon-input").value.trim();
const preview = document.getElementById("icon-preview");
preview.innerHTML = "";
preview.style.backgroundImage = "none";
preview.style.backgroundColor = "#ecf0f1";
if (!value) return;
try {
switch (type) {
case "svg":
const parser = new DOMParser();
const svgDoc = parser.parseFromString(value, "image/svg+xml");
if (svgDoc.querySelector("parsererror")) throw new Error("无效的SVG代码");
preview.innerHTML = value;
break;
case "image":
preview.style.backgroundImage = `url(${value})`;
preview.style.backgroundSize = "contain";
preview.style.backgroundRepeat = "no-repeat";
preview.style.backgroundPosition = "center";
break;
case "text":
const displayText = value.length > 4 ? value.substring(0, 4) : value;
preview.textContent = displayText;
preview.style.fontSize = value.length > 4 ? "14px" : "18px";
preview.style.color = "#2c3e50";
preview.style.fontWeight = "bold";
break;
case "emoji":
preview.textContent = value;
preview.style.fontSize = "24px";
break;
}
} catch (e) {
alert(`图标预览失败: ${e.message}`);
}
},
saveNewEngine() {
const name = document.getElementById("engine-name").value.trim();
const mark = document.getElementById("engine-mark").value.trim();
const url = document.getElementById("engine-url").value.trim();
const keys = document.getElementById("engine-keys").value.split(',').map(k => k.trim());
const iconType = document.getElementById("icon-type").value;
const iconValue = document.getElementById("icon-input").value.trim();
if (!name || !mark || !url || keys.length === 0) {
alert("请填写所有必填字段");
return;
}
if (appState.searchUrlMap.some(engine => engine.mark === mark)) {
alert("标识已存在,请使用其他标识");
return;
}
const newEngine = {
name,
searchUrl: url,
searchkeyName: keys,
matchUrl: new RegExp(`.*${new URL(url).hostname}.*`),
mark,
svgCode: "",
custom: true
};
if (iconValue) {
switch (iconType) {
case "svg":
newEngine.svgCode = iconValue;
break;
case "image":
newEngine.svgCode = ``;
break;
case "text":
newEngine.svgCode = ``;
break;
case "emoji":
newEngine.svgCode = ``;
break;
}
}
appState.userSearchEngines.push(newEngine);
GM_setValue(STORAGE_KEYS.USER_SEARCH_ENGINES, appState.userSearchEngines);
appState.searchUrlMap = [...defaultSearchEngines, ...appState.userSearchEngines];
const currentSetup = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK);
GM_setValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, `${currentSetup}-${mark}`);
utils.markUnsavedChanges();
alert("✅ 搜索引擎添加成功!");
this.showAddForm(false);
this.refreshEngineList();
},
resetToDefault() {
if (confirm("⚠️ 确定要恢复默认设置吗?这将删除所有自定义搜索引擎。")) {
appState.userSearchEngines = [];
GM_setValue(STORAGE_KEYS.USER_SEARCH_ENGINES, []);
GM_setValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK);
appState.searchUrlMap = [...defaultSearchEngines];
utils.markUnsavedChanges();
alert("✅ 已恢复默认设置");
this.refreshEngineList();
}
},
refreshEngineList() {
const engineList = document.getElementById("engine-management-list");
const activeMarks = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK).split("-");
if (!engineList) return;
engineList.innerHTML = "";
appState.searchUrlMap.forEach((engine) => {
const engineCard = document.createElement("div");
engineCard.className = CLASS_NAMES.ENGINE_CARD;
engineCard.style.cssText = `
display: flex;
align-items: center;
padding: 15px;
background: white;
border: 2px solid ${activeMarks.includes(engine.mark) ? '#27ae60' : '#ecf0f1'};
border-radius: 10px;
transition: all 0.3s ease;
cursor: grab;
min-height: 60px;
box-sizing: border-box;
`;
engineCard.addEventListener("mouseenter", () => {
engineCard.style.boxShadow = "0 4px 12px rgba(0,0,0,0.1)";
engineCard.style.transform = "translateY(-2px)";
});
engineCard.addEventListener("mouseleave", () => {
engineCard.style.boxShadow = "none";
engineCard.style.transform = "translateY(0)";
});
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.dataset.mark = engine.mark;
checkbox.checked = activeMarks.includes(engine.mark);
checkbox.style.cssText = `margin-right: 15px; transform: scale(1.2);`;
checkbox.addEventListener("change", () => {
utils.updateSelectedCount();
utils.markUnsavedChanges();
});
const iconPreview = document.createElement("div");
iconPreview.style.cssText = `
width: 40px;
height: 25px;
background-image: url('data:image/svg+xml;utf8,${encodeURIComponent(engine.svgCode)}');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
margin-right: 15px;
border: 1px solid #eee;
border-radius: 5px;
flex-shrink: 0;
`;
const infoContainer = document.createElement("div");
infoContainer.style.cssText = `flex-grow: 1; min-width: 0;`;
const name = document.createElement("div");
name.textContent = engine.name;
name.style.cssText = `
font-weight: bold;
color: #2c3e50;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const url = document.createElement("div");
url.textContent = engine.searchUrl;
url.style.cssText = `
font-size: 0.8em;
color: #7f8c8d;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
infoContainer.appendChild(name);
infoContainer.appendChild(url);
const actions = document.createElement("div");
actions.style.cssText = `display: flex; gap: 5px; flex-shrink: 0;`;
if (engine.custom) {
const deleteBtn = document.createElement("button");
deleteBtn.innerHTML = utils.createInlineSVG('trash', 'white');
deleteBtn.title = "删除";
deleteBtn.style.cssText = `
padding: 8px 12px;
border: none;
background: #e74c3c;
color: white;
border-radius: 5px;
cursor: pointer;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
`;
actions.appendChild(deleteBtn);
deleteBtn.addEventListener("click", (e) => {
e.stopPropagation();
if (confirm(`确定要删除 ${engine.name} 吗?`)) {
appState.userSearchEngines = appState.userSearchEngines.filter(e => e.mark !== engine.mark);
GM_setValue(STORAGE_KEYS.USER_SEARCH_ENGINES, appState.userSearchEngines);
const currentSetup = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK);
const newSetup = currentSetup.split("-").filter(m => m !== engine.mark).join("-");
GM_setValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, newSetup);
appState.searchUrlMap = [...defaultSearchEngines, ...appState.userSearchEngines];
utils.markUnsavedChanges();
this.refreshEngineList();
}
});
}
engineCard.appendChild(checkbox);
engineCard.appendChild(iconPreview);
engineCard.appendChild(infoContainer);
engineCard.appendChild(actions);
engineList.appendChild(engineCard);
});
utils.updateSelectedCount();
},
saveEngineSettings() {
const checkboxes = document.querySelectorAll('#engine-management-list input[type="checkbox"]');
const activeMarks = [];
checkboxes.forEach(checkbox => {
if (checkbox.checked) activeMarks.push(checkbox.dataset.mark);
});
if (activeMarks.length === 0) {
alert("⚠️ 请至少选择一个搜索引擎");
return;
}
GM_setValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, activeMarks.join("-"));
utils.clearUnsavedChanges();
setTimeout(() => {
this.closeManagementPanel();
appInitializer.reloadScript();
}, 1000);
},
closeManagementPanel() {
const panel = document.getElementById(CLASS_NAMES.MANAGEMENT_PANEL);
if (!panel) return;
if (appState.hasUnsavedChanges && !confirm("⚠️ 您有未保存的更改,确定要关闭吗?")) return;
panel.style.display = "none";
appState.hasUnsavedChanges = false;
accessibility.removeFocusTrap(panel);
},
createManagementPanel() {
let panel = document.getElementById(CLASS_NAMES.MANAGEMENT_PANEL);
if (panel) return panel;
panel = document.createElement("div");
panel.id = CLASS_NAMES.MANAGEMENT_PANEL;
panel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 800px;
height: 90vh;
max-height: 90vh;
background-color: #ffffff;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
padding: 0;
z-index: 10000;
display: none;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
flex-direction: column;
box-sizing: border-box;
`;
const header = document.createElement("div");
header.style.cssText = `
height: 15vh;
min-height: 80px;
max-height: 120px;
background-color: #2c3e50;
color: white;
padding: 20px;
border-radius: 15px 15px 0 0;
position: relative;
box-sizing: border-box;
flex-shrink: 0;
`;
const title = document.createElement("h2");
title.innerHTML = utils.createInlineSVG('cog', 'white') + ' 搜索引擎管理中心';
title.style.cssText = `
margin: 0;
font-size: 1.5em;
font-weight: 300;
display: flex;
align-items: center;
gap: 10px;
`;
const subtitle = document.createElement("p");
subtitle.textContent = "管理您的搜索快捷方式";
subtitle.style.cssText = `margin: 5px 0 0 0; opacity: 0.8; font-size: 0.9em;`;
const unsavedIndicator = document.createElement("div");
unsavedIndicator.id = "unsaved-indicator";
unsavedIndicator.innerHTML = utils.createInlineSVG('circle', '#e74c3c') + ' 有未保存的更改';
unsavedIndicator.style.cssText = `
position: absolute;
top: 15px;
right: 20px;
color: #e74c3c;
font-size: 0.8em;
display: none;
align-items: center;
gap: 5px;
`;
header.appendChild(title);
header.appendChild(subtitle);
header.appendChild(unsavedIndicator);
panel.appendChild(header);
const content = document.createElement("div");
content.style.cssText = `
height: 65vh;
min-height: 300px;
position: relative;
overflow: hidden;
padding: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
flex-shrink: 0;
`;
const quickActions = document.createElement("div");
quickActions.style.cssText = `
padding: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: space-between;
background-color: #ffffff;
border-bottom: 1px solid #ecf0f1;
box-sizing: border-box;
flex-shrink: 0;
`;
const leftActionGroup = document.createElement("div");
leftActionGroup.style.cssText = `display: flex; gap: 10px; flex-wrap: wrap;`;
const extractBtn = this.createActionButton(utils.createInlineSVG('globe') + ' 自动添加', "#3498db", "自动识别当前页面的搜索引擎");
const addBtn = this.createActionButton(utils.createInlineSVG('plus') + ' 手动添加', "#27ae60", "手动添加新的搜索引擎");
leftActionGroup.appendChild(extractBtn);
leftActionGroup.appendChild(addBtn);
const rightActionGroup = document.createElement("div");
rightActionGroup.style.cssText = `display: flex; gap: 10px; flex-wrap: wrap;`;
const saveBtn = document.createElement("button");
saveBtn.id = "panel-save-btn";
saveBtn.innerHTML = utils.createInlineSVG('save') + ' 保存设置';
saveBtn.title = "保存当前设置";
saveBtn.style.cssText = `
padding: 10px 20px;
background: #95a5a6;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 5px;
transition: all 0.3s ease;
opacity: 0.7;
pointer-events: none;
min-width: 120px;
justify-content: center;
`;
const resetBtn = this.createActionButton(utils.createInlineSVG('undo') + ' 恢复默认', "#e74c3c", "恢复默认搜索引擎设置");
rightActionGroup.appendChild(saveBtn);
rightActionGroup.appendChild(resetBtn);
quickActions.appendChild(leftActionGroup);
quickActions.appendChild(rightActionGroup);
content.appendChild(quickActions);
const listSection = document.createElement("div");
listSection.style.cssText = `
flex: 1;
overflow: hidden;
padding: 0 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: auto;
`;
const listTitle = document.createElement("h3");
listTitle.innerHTML = utils.createInlineSVG('list') + ' 已配置的搜索引擎';
listTitle.style.cssText = `
color: #2c3e50;
margin: 15px 0;
font-weight: 500;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 10px;
`;
const engineList = document.createElement("div");
engineList.id = "engine-management-list";
engineList.style.cssText = `
flex: 1;
overflow-y: auto;
overflow-x: hidden;
display: grid;
gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
padding-bottom: 10px;
box-sizing: border-box;
`;
listSection.appendChild(listTitle);
listSection.appendChild(engineList);
const formSection = document.createElement("div");
formSection.id = "add-engine-form";
formSection.style.cssText = `
display: none;
background-color: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin: 10px 0;
box-sizing: border-box;
flex-shrink: 0;
`;
const formTitle = document.createElement("h3");
formTitle.innerHTML = utils.createInlineSVG('magic') + ' 添加新搜索引擎';
formTitle.style.cssText = `
color: #2c3e50;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
`;
formSection.appendChild(formTitle);
const form = document.createElement("div");
form.style.cssText = `display: grid; gap: 15px; grid-template-columns: 1fr 1fr;`;
const fields = [
{ label: "引擎名称", placeholder: "例如: Google", type: "text", id: "engine-name", required: true },
{ label: "唯一标识", placeholder: "例如: google", type: "text", id: "engine-mark", required: true },
{ label: "搜索URL", placeholder: "使用 {keyword} 作为占位符", type: "text", id: "engine-url", required: true, fullWidth: true },
{ label: "关键词参数", placeholder: "例如: q,query,search", type: "text", id: "engine-keys", required: true, fullWidth: true }
];
fields.forEach(field => {
const container = document.createElement("div");
if (field.fullWidth) container.style.gridColumn = "1 / -1";
const label = document.createElement("label");
label.textContent = field.label;
label.style.cssText = `display: block; margin-bottom: 5px; font-weight: 500; color: #34495e;`;
const input = document.createElement("input");
input.type = field.type;
input.placeholder = field.placeholder;
input.id = field.id;
input.required = field.required;
input.style.cssText = `width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px;`;
container.appendChild(label);
container.appendChild(input);
form.appendChild(container);
});
const iconContainer = document.createElement("div");
iconContainer.style.gridColumn = "1 / -1";
const iconTitle = document.createElement("h4");
iconTitle.innerHTML = utils.createInlineSVG('palette') + ' 图标设置';
iconTitle.style.cssText = `margin-bottom: 10px; color: #34495e; display: flex; align-items: center; gap: 10px;`;
iconContainer.appendChild(iconTitle);
const iconGrid = document.createElement("div");
iconGrid.style.cssText = `display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 10px; align-items: end;`;
const typeGroup = document.createElement("div");
const typeLabel = document.createElement("label");
typeLabel.textContent = "图标类型";
typeLabel.style.cssText = `display: block; margin-bottom: 5px; font-weight: 500;`;
typeGroup.appendChild(typeLabel);
const iconTypeSelect = document.createElement("select");
iconTypeSelect.id = "icon-type";
iconTypeSelect.style.cssText = `width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px;`;
["svg", "image", "text", "emoji"].forEach(type => {
const option = document.createElement("option");
option.value = type;
option.textContent = type.charAt(0).toUpperCase() + type.slice(1);
iconTypeSelect.appendChild(option);
});
typeGroup.appendChild(iconTypeSelect);
const inputGroup = document.createElement("div");
const inputLabel = document.createElement("label");
inputLabel.textContent = "图标内容";
inputLabel.style.cssText = `display: block; margin-bottom: 5px; font-weight: 500;`;
inputGroup.appendChild(inputLabel);
const iconInput = document.createElement("input");
iconInput.type = "text";
iconInput.id = "icon-input";
iconInput.placeholder = "SVG代码、图片URL、文字或表情符号";
iconInput.style.cssText = `width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px;`;
inputGroup.appendChild(iconInput);
const previewGroup = document.createElement("div");
const previewButton = document.createElement("button");
previewButton.innerHTML = utils.createInlineSVG('eye') + ' 预览图标';
previewButton.style.cssText = `
width: 100%;
padding: 10px;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
`;
previewButton.id = "preview-icon";
previewGroup.appendChild(previewButton);
// 组装图标设置网格
iconGrid.appendChild(typeGroup);
iconGrid.appendChild(inputGroup);
iconGrid.appendChild(previewGroup);
iconContainer.appendChild(iconGrid);
// 图标预览区域
const previewContainer = document.createElement("div");
previewContainer.style.gridColumn = "1 / -1";
previewContainer.style.cssText = `
margin-top: 15px;
text-align: center;
`;
const previewLabel = document.createElement("label");
previewLabel.textContent = "图标预览 (推荐比例 8:5)";
previewLabel.style.cssText = `
display: block;
margin-bottom: 10px;
font-weight: 500;
`;
const iconPreview = document.createElement("div");
iconPreview.id = "icon-preview";
iconPreview.style.cssText = `
width: 88px;
height: 55px;
border: 2px dashed #bdc3c7;
border-radius: 8px;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
background: #ecf0f1;
`;
previewContainer.appendChild(previewLabel);
previewContainer.appendChild(iconPreview);
iconContainer.appendChild(previewContainer);
form.appendChild(iconContainer);
// 表单操作按钮
const formActions = document.createElement("div");
formActions.style.cssText = `
grid-column: 1 / -1;
display: flex;
gap: 10px;
margin-top: 20px;
`;
const saveFormBtn = this.createActionButton(utils.createInlineSVG('save') + ' 保存引擎', "#27ae60", "");
const cancelFormBtn = this.createActionButton(utils.createInlineSVG('times') + ' 取消', "#95a5a6", "");
formActions.appendChild(saveFormBtn);
formActions.appendChild(cancelFormBtn);
formSection.appendChild(form);
formSection.appendChild(formActions);
listSection.appendChild(formSection);
content.appendChild(listSection);
panel.appendChild(content);
// 4. 面板底部
const footer = document.createElement("div");
footer.style.cssText = `
height: 20vh;
min-height: 60px;
max-height: 90px;
background-color: #ecf0f1;
padding: 15px 20px;
border-top: 1px solid #bdc3c7;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
flex-shrink: 0;
border-radius: 0 0 15px 15px;
`;
const selectedCount = document.createElement("span");
selectedCount.id = "selected-count";
selectedCount.innerHTML = utils.createInlineSVG('check-circle') + ' 已选择 0 个引擎';
selectedCount.style.cssText = `
color: #7f8c8d;
font-size: 0.9em;
display: flex;
align-items: center;
gap: 5px;
`;
const footerActions = document.createElement("div");
footerActions.style.cssText = `
display: flex;
gap: 10px;
`;
const closeBtn = this.createActionButton(utils.createInlineSVG('times') + ' 关闭', "#95a5a6", "");
footerActions.appendChild(closeBtn);
footer.appendChild(selectedCount);
footer.appendChild(footerActions);
panel.appendChild(footer);
// 5. 绑定事件
extractBtn.addEventListener("click", () => this.extractFromCurrentPage());
addBtn.addEventListener("click", () => this.showAddForm(true));
resetBtn.addEventListener("click", () => this.resetToDefault());
previewButton.addEventListener("click", () => this.previewIcon());
saveFormBtn.addEventListener("click", () => this.saveNewEngine());
cancelFormBtn.addEventListener("click", () => this.showAddForm(false));
saveBtn.addEventListener("click", () => this.saveEngineSettings());
closeBtn.addEventListener("click", () => this.closeManagementPanel());
// 点击面板背景关闭
panel.addEventListener("click", (e) => {
if (e.target === panel) {
this.closeManagementPanel();
}
});
document.body.appendChild(panel);
return panel;
},
/**
* 显示管理面板
*/
showManagementPanel() {
const panel = this.createManagementPanel();
// 重置未保存状态
appState.hasUnsavedChanges = false;
utils.clearUnsavedChanges();
// 刷新引擎列表
this.refreshEngineList();
// 显示面板
panel.style.display = "block";
// 应用焦点陷阱
accessibility.trapFocus(panel);
// 隐藏汉堡菜单
hamburgerMenu.hideHamburgerMenu();
},
/**
* 创建管理面板DOM结构(核心配置界面)
*/
createManagementPanel() {
let panel = document.getElementById(CLASS_NAMES.MANAGEMENT_PANEL);
if (panel) return panel;
// 1. 面板主容器
panel = document.createElement("div");
panel.id = CLASS_NAMES.MANAGEMENT_PANEL;
panel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 90%;
max-width: 800px;
height: 90vh;
max-height: 90vh;
background-color: #ffffff;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
padding: 0;
z-index: 10000;
display: none;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
display: flex;
flex-direction: column;
box-sizing: border-box;
`;
// 2. 面板头部
const header = document.createElement("div");
header.style.cssText = `
height: 15vh;
min-height: 80px;
max-height: 120px;
background-color: #2c3e50;
color: white;
padding: 20px;
border-radius: 15px 15px 0 0;
position: relative;
box-sizing: border-box;
flex-shrink: 0;
`;
const title = document.createElement("h2");
title.innerHTML = utils.createInlineSVG('cog', 'white') + ' 搜索引擎管理中心';
title.style.cssText = `
margin: 0;
font-size: 1.5em;
font-weight: 300;
display: flex;
align-items: center;
gap: 10px;
`;
const subtitle = document.createElement("p");
subtitle.textContent = "管理您的搜索快捷方式";
subtitle.style.cssText = `
margin: 5px 0 0 0;
opacity: 0.8;
font-size: 0.9em;
`;
// 未保存更改指示器
const unsavedIndicator = document.createElement("div");
unsavedIndicator.id = "unsaved-indicator";
unsavedIndicator.innerHTML = utils.createInlineSVG('circle', '#e74c3c') + ' 有未保存的更改';
unsavedIndicator.style.cssText = `
position: absolute;
top: 15px;
right: 20px;
color: #e74c3c;
font-size: 0.8em;
display: none;
align-items: center;
gap: 5px;
`;
header.appendChild(title);
header.appendChild(subtitle);
header.appendChild(unsavedIndicator);
panel.appendChild(header);
// 3. 面板内容区
const content = document.createElement("div");
content.style.cssText = `
height: 65vh;
min-height: 300px;
position: relative;
overflow: hidden;
padding: 0;
box-sizing: border-box;
display: flex;
flex-direction: column;
flex-shrink: 0;
`;
// 3.1 快捷操作栏
const quickActions = document.createElement("div");
quickActions.style.cssText = `
padding: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: space-between;
background-color: #ffffff;
border-bottom: 1px solid #ecf0f1;
box-sizing: border-box;
flex-shrink: 0;
`;
// 左侧操作组
const leftActionGroup = document.createElement("div");
leftActionGroup.style.cssText = `
display: flex;
gap: 10px;
flex-wrap: wrap;
`;
const extractBtn = this.createActionButton(utils.createInlineSVG('globe') + ' 自动添加', "#3498db", "自动识别当前页面的搜索引擎");
const addBtn = this.createActionButton(utils.createInlineSVG('plus') + ' 手动添加', "#27ae60", "手动添加新的搜索引擎");
leftActionGroup.appendChild(extractBtn);
leftActionGroup.appendChild(addBtn);
// 右侧操作组
const rightActionGroup = document.createElement("div");
rightActionGroup.style.cssText = `
display: flex;
gap: 10px;
flex-wrap: wrap;
`;
const saveBtn = document.createElement("button");
saveBtn.id = "panel-save-btn";
saveBtn.innerHTML = utils.createInlineSVG('save') + ' 保存设置';
saveBtn.title = "保存当前设置";
saveBtn.style.cssText = `
padding: 10px 20px;
background: #95a5a6;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
display: flex;
align-items: center;
gap: 5px;
transition: all 0.3s ease;
opacity: 0.7;
pointer-events: none;
min-width: 120px;
justify-content: center;
`;
const resetBtn = this.createActionButton(utils.createInlineSVG('undo') + ' 恢复默认', "#e74c3c", "恢复默认搜索引擎设置");
rightActionGroup.appendChild(saveBtn);
rightActionGroup.appendChild(resetBtn);
quickActions.appendChild(leftActionGroup);
quickActions.appendChild(rightActionGroup);
content.appendChild(quickActions);
// 3.2 引擎列表区
const listSection = document.createElement("div");
listSection.style.cssText = `
flex: 1;
overflow: hidden;
padding: 0 20px;
box-sizing: border-box;
display: flex;
flex-direction: column;
overflow: auto;
`;
const listTitle = document.createElement("h3");
listTitle.innerHTML = utils.createInlineSVG('list') + ' 已配置的搜索引擎';
listTitle.style.cssText = `
color: #2c3e50;
margin: 15px 0;
font-weight: 500;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 10px;
`;
const engineList = document.createElement("div");
engineList.id = "engine-management-list";
engineList.style.cssText = `
flex: 1;
overflow-y: auto;
overflow-x: hidden;
display: grid;
gap: 10px;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
padding-bottom: 10px;
box-sizing: border-box;
`;
listSection.appendChild(listTitle);
listSection.appendChild(engineList);
// 3.3 添加引擎表单
const formSection = document.createElement("div");
formSection.id = "add-engine-form";
formSection.style.cssText = `
display: none;
background-color: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin: 10px 0;
box-sizing: border-box;
flex-shrink: 0;
`;
const formTitle = document.createElement("h3");
formTitle.innerHTML = utils.createInlineSVG('magic') + ' 添加新搜索引擎';
formTitle.style.cssText = `
color: #2c3e50;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
`;
formSection.appendChild(formTitle);
// 表单字段容器
const form = document.createElement("div");
form.style.cssText = `
display: grid;
gap: 15px;
grid-template-columns: 1fr 1fr;
`;
// 表单字段配置
const fields = [{
label: "引擎名称",
placeholder: "例如: Google",
type: "text",
id: "engine-name",
required: true
},
{
label: "唯一标识",
placeholder: "例如: google",
type: "text",
id: "engine-mark",
required: true
},
{
label: "搜索URL",
placeholder: "使用 {keyword} 作为占位符",
type: "text",
id: "engine-url",
required: true,
fullWidth: true
},
{
label: "关键词参数",
placeholder: "例如: q,query,search",
type: "text",
id: "engine-keys",
required: true,
fullWidth: true
}
];
// 创建表单字段
fields.forEach(field => {
const container = document.createElement("div");
if (field.fullWidth) {
container.style.gridColumn = "1 / -1";
}
const label = document.createElement("label");
label.textContent = field.label;
label.style.cssText = `
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #34495e;
`;
const input = document.createElement("input");
input.type = field.type;
input.placeholder = field.placeholder;
input.id = field.id;
input.required = field.required;
input.style.cssText = `
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 14px;
`;
container.appendChild(label);
container.appendChild(input);
form.appendChild(container);
});
// 图标设置区域
const iconContainer = document.createElement("div");
iconContainer.style.gridColumn = "1 / -1";
const iconTitle = document.createElement("h4");
iconTitle.innerHTML = utils.createInlineSVG('palette') + ' 图标设置';
iconTitle.style.cssText = `
margin-bottom: 10px;
color: #34495e;
display: flex;
align-items: center;
gap: 10px;
`;
iconContainer.appendChild(iconTitle);
// 图标设置网格
const iconGrid = document.createElement("div");
iconGrid.style.cssText = `
display: grid;
grid-template-columns: 1fr 2fr 1fr;
gap: 10px;
align-items: end;
`;
// 图标类型选择
const typeGroup = document.createElement("div");
const typeLabel = document.createElement("label");
typeLabel.textContent = "图标类型";
typeLabel.style.cssText = `
display: block;
margin-bottom: 5px;
font-weight: 500;
`;
typeGroup.appendChild(typeLabel);
const iconTypeSelect = document.createElement("select");
iconTypeSelect.id = "icon-type";
iconTypeSelect.style.cssText = `
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
`;
["svg", "image", "text", "emoji"].forEach(type => {
const option = document.createElement("option");
option.value = type;
option.textContent = type.charAt(0).toUpperCase() + type.slice(1);
iconTypeSelect.appendChild(option);
});
typeGroup.appendChild(iconTypeSelect);
// 图标内容输入
const inputGroup = document.createElement("div");
const inputLabel = document.createElement("label");
inputLabel.textContent = "图标内容";
inputLabel.style.cssText = `
display: block;
margin-bottom: 5px;
font-weight: 500;
`;
inputGroup.appendChild(inputLabel);
const iconInput = document.createElement("input");
iconInput.type = "text";
iconInput.id = "icon-input";
iconInput.placeholder = "SVG代码、图片URL、文字或表情符号";
iconInput.style.cssText = `
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
`;
inputGroup.appendChild(iconInput);
// 预览按钮
const previewGroup = document.createElement("div");
const previewButton = document.createElement("button");
previewButton.innerHTML = utils.createInlineSVG('eye') + ' 预览图标';
previewButton.style.cssText = `
width: 100%;
padding: 10px;
background-color: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
`;
previewButton.id = "preview-icon";
previewGroup.appendChild(previewButton);
// 组装图标设置网格
iconGrid.appendChild(typeGroup);
iconGrid.appendChild(inputGroup);
iconGrid.appendChild(previewGroup);
iconContainer.appendChild(iconGrid);
// 图标预览区域
const previewContainer = document.createElement("div");
previewContainer.style.gridColumn = "1 / -1";
previewContainer.style.cssText = `
margin-top: 15px;
text-align: center;
`;
const previewLabel = document.createElement("label");
previewLabel.textContent = "图标预览 (推荐比例 8:5)";
previewLabel.style.cssText = `
display: block;
margin-bottom: 10px;
font-weight: 500;
`;
const iconPreview = document.createElement("div");
iconPreview.id = "icon-preview";
iconPreview.style.cssText = `
width: 88px;
height: 55px;
border: 2px dashed #bdc3c7;
border-radius: 8px;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
background: #ecf0f1;
`;
previewContainer.appendChild(previewLabel);
previewContainer.appendChild(iconPreview);
iconContainer.appendChild(previewContainer);
form.appendChild(iconContainer);
// 表单操作按钮
const formActions = document.createElement("div");
formActions.style.cssText = `
grid-column: 1 / -1;
display: flex;
gap: 10px;
margin-top: 20px;
`;
const saveFormBtn = this.createActionButton(utils.createInlineSVG('save') + ' 保存引擎', "#27ae60", "");
const cancelFormBtn = this.createActionButton(utils.createInlineSVG('times') + ' 取消', "#95a5a6", "");
formActions.appendChild(saveFormBtn);
formActions.appendChild(cancelFormBtn);
formSection.appendChild(form);
formSection.appendChild(formActions);
listSection.appendChild(formSection);
content.appendChild(listSection);
panel.appendChild(content);
// 4. 面板底部
const footer = document.createElement("div");
footer.style.cssText = `
height: 20vh;
min-height: 60px;
max-height: 90px;
background-color: #ecf0f1;
padding: 15px 20px;
border-top: 1px solid #bdc3c7;
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
flex-shrink: 0;
border-radius: 0 0 15px 15px;
`;
const selectedCount = document.createElement("span");
selectedCount.id = "selected-count";
selectedCount.innerHTML = utils.createInlineSVG('check-circle') + ' 已选择 0 个引擎';
selectedCount.style.cssText = `
color: #7f8c8d;
font-size: 0.9em;
display: flex;
align-items: center;
gap: 5px;
`;
const footerActions = document.createElement("div");
footerActions.style.cssText = `
display: flex;
gap: 10px;
`;
const closeBtn = this.createActionButton(utils.createInlineSVG('times') + ' 关闭', "#95a5a6", "");
footerActions.appendChild(closeBtn);
footer.appendChild(selectedCount);
footer.appendChild(footerActions);
panel.appendChild(footer);
// 5. 绑定事件
extractBtn.addEventListener("click", () => this.extractFromCurrentPage());
addBtn.addEventListener("click", () => this.showAddForm(true));
resetBtn.addEventListener("click", () => this.resetToDefault());
previewButton.addEventListener("click", () => this.previewIcon());
saveFormBtn.addEventListener("click", () => this.saveNewEngine());
cancelFormBtn.addEventListener("click", () => this.showAddForm(false));
saveBtn.addEventListener("click", () => this.saveEngineSettings());
closeBtn.addEventListener("click", () => this.closeManagementPanel());
// 点击面板背景关闭
panel.addEventListener("click", (e) => {
if (e.target === panel) {
this.closeManagementPanel();
}
});
document.body.appendChild(panel);
return panel;
},
/**
* 显示管理面板
*/
showManagementPanel() {
const panel = this.createManagementPanel();
// 重置未保存状态
appState.hasUnsavedChanges = false;
utils.clearUnsavedChanges();
// 刷新引擎列表
this.refreshEngineList();
// 显示面板
panel.style.display = "block";
// 应用焦点陷阱
accessibility.trapFocus(panel);
// 隐藏汉堡菜单
hamburgerMenu.hideHamburgerMenu();
}
};
// ===== 应用初始化模块 =====
/**
* 应用初始化模块 - 封装初始化、脚本重载、页面事件监听等入口逻辑
*/
const appInitializer = {
/**
* 重新加载脚本(清理DOM、重置状态、重新初始化)
*/
reloadScript() {
// 1. 清理所有创建的DOM元素
[
"#punkjet-search-box",
`#${CLASS_NAMES.HAMBURGER_MENU}`,
`#${CLASS_NAMES.SEARCH_OVERLAY}`,
`#${CLASS_NAMES.MANAGEMENT_PANEL}`
].forEach(selector => {
const element = document.querySelector(selector);
if (element) {
// 移除焦点陷阱
accessibility.removeFocusTrap(element);
element.remove();
}
});
// 2. 清除所有定时器和防抖器
utils.clearAllTimeouts();
debounceUtils.clearAll();
// 3. 移除全局事件监听器
const events = ['scroll', 'wheel', 'touchstart', 'touchmove', 'touchend'];
events.forEach(event => {
window.removeEventListener(event, () => {});
});
// 4. 重置应用状态
appState.scriptLoaded = false;
appState.containerAdded = false;
appState.hamburgerMenuOpen = false;
appState.searchOverlayVisible = false;
// 5. 重新初始化
this.init();
},
/**
* 百度搜索特殊处理(延迟同步输入框内容)
*/
handleBaiduSpecialCase() {
if (window.location.hostname.includes('baidu')) {
setTimeout(() => {
const baiduInput = document.querySelector('input#kw');
if (baiduInput && baiduInput.value) {
appState.currentInput = baiduInput.value.trim();
sessionStorage.setItem(STORAGE_KEYS.CURRENT_INPUT, appState.currentInput);
}
}, DEFAULT_CONFIG.BAIDU_INPUT_DELAY);
}
},
/**
* 初始化应用
*/
init() {
try {
// 前置校验
if (appState.containerAdded || appState.scriptLoaded) {
return;
}
// 初始化搜索引擎功能
if (utils.isValidScope()) {
// 1. 初始化默认存储配置
if (!GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH)) {
GM_setValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK);
}
// 2. 从sessionStorage恢复当前输入内容
appState.currentInput = sessionStorage.getItem(STORAGE_KEYS.CURRENT_INPUT) || '';
// 3. 执行初始化流程
domHandler.monitorInputFields();
domHandler.addSearchBox();
domHandler.injectStyle();
accessibility.init();
this.handleBaiduSpecialCase();
// 4. 更新初始化状态
appState.scriptLoaded = true;
// 应用引擎排序
setTimeout(() => {
hamburgerMenu.applyEngineSort();
}, 500);
}
// 初始化 GitHub 增强功能
githubEnhancer.init();
} catch (error) {
console.error("应用初始化失败:", error.message);
}
},
/**
* 初始化页面事件监听( visibilitychange、pageshow 等)
*/
initPageEventListeners() {
// 1. 页面可见性变化时重新检查初始化
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === 'visible' && !appState.containerAdded) {
this.init();
}
});
// 2. 页面从缓存恢复时重新检查初始化
document.addEventListener("pageshow", (event) => {
if (event.persisted && !appState.containerAdded) {
this.init();
}
});
// 3. DOM加载完成后初始化
document.addEventListener("DOMContentLoaded", () => {
if (utils.isValidScope()) {
this.init();
}
});
// 4. 定期检查作用域(确保页面动态变化后仍能正常初始化)
setInterval(() => {
if (utils.isValidScope() && !appState.containerAdded) {
this.init();
} else if (!utils.isValidScope() && appState.containerAdded) {
this.reloadScript();
}
}, DEFAULT_CONFIG.CHECK_SCOPE_INTERVAL);
}
};
// ===== 应用启动入口 =====
// 初始化应用状态
const appState = {
userSearchEngines: GM_getValue(STORAGE_KEYS.USER_SEARCH_ENGINES, []),
searchUrlMap: [...defaultSearchEngines, ...GM_getValue(STORAGE_KEYS.USER_SEARCH_ENGINES, [])],
lastScrollTop: 0,
punkJetBoxVisible: true,
currentInput: sessionStorage.getItem(STORAGE_KEYS.CURRENT_INPUT) || '',
scriptLoaded: false,
containerAdded: false,
hasUnsavedChanges: false,
scrollTimeout: null,
isScrolling: false,
hideTimeout: null,
touchStartY: null,
hamburgerMenuOpen: false,
searchOverlayVisible: false,
isInteractingWithEngineBar: false
};
// 启动应用
appInitializer.initPageEventListeners();