// ==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 = ` ${iconValue} `; break; case "emoji": newEngine.svgCode = ` ${iconValue} `; 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();