// ==UserScript== // @name 聚合搜索引擎切换导航 + GitHub增强(移动端优化) // @namespace http://tampermonkey.net/ // @version v2.1.9 // @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搜索结果显示部署网站和发布版本标签、拖拽排序、自定义引擎管理、快捷搜索、智能排序、无障碍访问 // ==/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(), releasesCache: new Map(), // Cache: repoId → boolean (has releases/tags) 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 !important; flex-wrap: wrap !important; align-items: center !important; margin-top: 6px !important; margin-bottom: 4px !important; gap: 6px !important; 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) => { const timeout = setTimeout(() => { reject(new Error('Request timeout')); }, 15000); GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 15000, headers: { 'Accept': 'application/vnd.github.v3+json' }, onload: (response) => { clearTimeout(timeout); if (response.status === 200) { resolve(response.responseText); } else if (response.status === 301 || response.status === 302 || response.status === 307 || response.status === 308) { // Follow redirect — GitHub API returns 301 for renamed repos (e.g. facebook/react → repositories/10270250) const location = response.responseHeaders.match(/location:\s*(.+)/i); if (location && location[1]) { this.makeRequest(location[1].trim()).then(resolve).catch(reject); } else { reject(new Error(`Redirect without location`)); } } else { reject(new Error(`HTTP ${response.status}`)); } }, onerror: (err) => { clearTimeout(timeout); reject(err); }, ontimeout: () => { clearTimeout(timeout); reject(new Error('Request timeout')); } }); }); }, 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 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) { const repoId = `${repoOwner}/${repoName}`; // Return cached result if available if (this.releasesCache.has(repoId)) { return this.releasesCache.get(repoId); } try { // Try releases API first const releasesData = await this.makeRequest(`https://api.github.com/repos/${repoOwner}/${repoName}/releases?per_page=1`); const releases = JSON.parse(releasesData); if (Array.isArray(releases) && releases.length > 0) { this.releasesCache.set(repoId, true); return true; } // Releases API returned empty — check tags API as fallback const tagsData = await this.makeRequest(`https://api.github.com/repos/${repoOwner}/${repoName}/tags?per_page=1`); const tags = JSON.parse(tagsData); const hasTags = Array.isArray(tags) && tags.length > 0; this.releasesCache.set(repoId, hasTags); return hasTags; } catch (e) { // API failed (likely rate limited at 60 req/hour, or network error, or 301 redirect) // checkDeployment succeeds via raw.githubusercontent.com (not rate limited) // but checkReleases only uses the API which IS rate limited // Default to true: most repos on GitHub search results have releases or tags // Better to show the tag and let user discover than to hide it when it exists this.releasesCache.set(repoId, true); return true; } }, 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 createContainer = () => { const container = document.createElement('div'); container.className = 'github-tags-container'; return container; }; const positions = [ // 1st: GitHub CSS Modules — description content block (stable prefix) () => { const description = repoItem.querySelector('[class*="Content-module__Content__"]'); if (description && description.textContent.trim()) { const container = createContainer(); description.parentNode.insertBefore(container, description.nextSibling); return container; } return null; }, // 2nd: Structural fallback — description is the
right after

() => { const h3 = repoItem.querySelector('h3'); if (h3 && h3.nextElementSibling && h3.nextElementSibling.tagName === 'DIV') { const desc = h3.nextElementSibling; if (desc.textContent.trim()) { const container = createContainer(); desc.parentNode.insertBefore(container, desc.nextSibling); return container; } } return null; }, // 3rd: Legacy description selectors (older GitHub layouts or other pages) () => { const description = repoItem.querySelector('[class*="description"], .jsbtiO, [class*="repo-description"]'); if (description) { const container = createContainer(); description.parentNode.insertBefore(container, description.nextSibling); return container; } return null; }, // 4th: Footer/metadata area (stars, language, updated time) () => { const footer = repoItem.querySelector('[class*="Footer-module__footer__"], [class*="metadata"], .dmuROe, .gbntE'); if (footer) { const container = createContainer(); footer.parentNode.insertBefore(container, footer); return container; } return null; }, // 5th: Fallback — append to result content column () => { const contentCol = repoItem.querySelector('[class*="Repositories-module__resultContent__"]') || repoItem; const container = createContainer(); contentCol.appendChild(container); return container; } ]; 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 = [ '[data-testid="results-list"] [class*="Result-module__Result__"]', '[class*="Result-module__Result__"]', '.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() { let debounceTimer = null; const observer = new MutationObserver((mutations) => { let shouldProcess = false; for (const mutation of mutations) { if (mutation.addedNodes.length === 0) continue; for (const node of mutation.addedNodes) { if (node.nodeType !== 1) continue; if ((node.querySelector && ( node.querySelector('.fXzjPH') || node.querySelector('[data-testid="repository-card"]') || node.querySelector('a[href*="/"][href*="/"]') )) || (node.matches && ( node.matches('.fXzjPH') || node.matches('[data-testid="repository-card"]') ))) { shouldProcess = true; break; } } if (shouldProcess) break; } if (shouldProcess) { if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(() => this.processVisibleRepos(), 500); } }); observer.observe(document.body, { childList: true, subtree: true }); return observer; } }; 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 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 }; // ===== 可访问性模块 ===== 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 === 'flex') 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(() => { debounceUtils.debounce('aria_enhance', () => this.enhanceAriaLabels(), 500); }); 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') + ' 保存更改'; if (!saveBtn._hoverHandlers) { 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._hoverHandlers = { enter: () => handleHover.call(saveBtn, true), leave: () => handleHover.call(saveBtn, false) }; } saveBtn.removeEventListener("mouseenter", saveBtn._hoverHandlers.enter); saveBtn.removeEventListener("mouseleave", saveBtn._hoverHandlers.leave); saveBtn.addEventListener("mouseenter", saveBtn._hoverHandlers.enter); saveBtn.addEventListener("mouseleave", saveBtn._hoverHandlers.leave); } }, 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}`); // Get the marks of currently displayed engines in their DOM order const displayedMarks = Array.from(buttons) .map(btn => btn.getAttribute('data-mark')) .filter(mark => mark !== null); // Merge with any marks not currently displayed (preserving their relative order at the end) const currentOrder = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK).split('-'); const hiddenMarks = currentOrder.filter(mark => !displayedMarks.includes(mark)); const newOrder = [...displayedMarks, ...hiddenMarks].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: 0; 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 .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: 0; 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,.1), 0 0 0 rgba(255,255,255,.5), 6px 6px 10px rgba(0,0,0,.1) inset, -6px -6px 10px rgba(255,255,255,0) inset; transition: all .3s ease; flex-shrink: 0; overflow: hidden; } .${CLASS_NAMES.ENGINE_BUTTON}:focus, .${CLASS_NAMES.ENGINE_BUTTON}.selected { border: 2px dashed #2196F3; background-color: #f0f8ff; } .${CLASS_NAMES.ENGINE_BUTTON}[draggable="true"] { cursor: grab; } .${CLASS_NAMES.ENGINE_BUTTON}[draggable="false"] { cursor: default; } .${CLASS_NAMES.ENGINE_BUTTON}.${CLASS_NAMES.DRAGGING} { opacity: .3; transform: scale(.92); cursor: grabbing; } .${CLASS_NAMES.ENGINE_BUTTON}.${CLASS_NAMES.DRAG_OVER} { border: 2px solid #6366f1; background-color: #eef2ff; transform: scale(1.1); } .pk-drop-indicator { width: 3px; height: 32px; background: #6366f1; border-radius: 2px; flex-shrink: 0; margin: 2px 1px; animation: pulse 0.6s ease infinite; box-shadow: 0 0 8px rgba(99,102,241,.5); } .${CLASS_NAMES.ENGINE_CARD} { transition: all .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; } } @keyframes scaleIn { from { opacity: 0; transform: scale(.92); } to { opacity: 1; transform: scale(1); } } @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .5; } } #${CLASS_NAMES.MANAGEMENT_PANEL} { animation: slideIn .35s cubic-bezier(.16,1,.3,1); } #${CLASS_NAMES.HAMBURGER_MENU} { animation: slideInLeft .3s ease; } #${CLASS_NAMES.SEARCH_OVERLAY} { animation: fadeIn .25s ease; } /* ========== CSS 变量(亮色/暗色) ========== */ :root { --pk-bg: #f0f2f5; --pk-surface: #ffffff; --pk-surface-alt: #f8f9fa; --pk-text: #1a1a2e; --pk-text-secondary: #64748b; --pk-text-muted: #94a3b8; --pk-border: #e2e8f0; --pk-border-light: #f1f5f9; --pk-primary: #6366f1; --pk-primary-light: #818cf8; --pk-primary-dark: #4f46e5; --pk-accent: #ec4899; --pk-success: #10b981; --pk-danger: #ef4444; --pk-warning: #f59e0b; --pk-shadow-sm: 0 1px 2px rgba(0,0,0,.05); --pk-shadow: 0 4px 6px -1px rgba(0,0,0,.1), 0 2px 4px -1px rgba(0,0,0,.06); --pk-shadow-lg: 0 10px 30px -3px rgba(0,0,0,.1), 0 4px 6px -2px rgba(0,0,0,.05); --pk-shadow-xl: 0 20px 50px -10px rgba(0,0,0,.15), 0 8px 16px -4px rgba(0,0,0,.06); --pk-radius: 12px; --pk-radius-lg: 16px; --pk-radius-xl: 24px; --pk-gradient: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #ec4899 100%); --pk-gradient-soft: linear-gradient(135deg, rgba(99,102,241,.08), rgba(236,72,153,.08)); } @media (prefers-color-scheme: dark) { :root { --pk-bg: #0f172a; --pk-surface: #1e293b; --pk-surface-alt: #334155; --pk-text: #f1f5f9; --pk-text-secondary: #cbd5e1; --pk-text-muted: #94a3b8; --pk-border: #334155; --pk-border-light: #1e293b; --pk-shadow-sm: 0 1px 2px rgba(0,0,0,.3); --pk-shadow: 0 4px 6px -1px rgba(0,0,0,.4), 0 2px 4px -1px rgba(0,0,0,.3); --pk-shadow-lg: 0 10px 30px -3px rgba(0,0,0,.5), 0 4px 6px -2px rgba(0,0,0,.3); --pk-shadow-xl: 0 20px 50px -10px rgba(0,0,0,.6), 0 8px 16px -4px rgba(0,0,0,.4); --pk-gradient-soft: linear-gradient(135deg, rgba(99,102,241,.15), rgba(236,72,153,.15)); } } /* ========== 搜索遮罩层 ========== */ #${CLASS_NAMES.SEARCH_OVERLAY} { position: fixed; inset: 0; z-index: 9998; display: none; background: rgba(15,23,42,.6); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .pk-overlay-scroll { width: 100%; height: 100%; overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; display: flex; align-items: flex-start; justify-content: center; padding: 4vh 16px; box-sizing: border-box; } .pk-search-card { width: 100%; max-width: 820px; background: var(--pk-surface); border-radius: var(--pk-radius-xl); padding: 24px 28px 20px; box-shadow: var(--pk-shadow-xl); border: 1px solid var(--pk-border); position: relative; box-sizing: border-box; animation: scaleIn .35s cubic-bezier(.16,1,.3,1); } @media (max-width: 768px) { .pk-search-card { padding: 20px 16px 16px; border-radius: var(--pk-radius-lg); max-width: 94%; } .pk-overlay-scroll { padding: 2vh 10px; } } .pk-close-btn { position: absolute; top: 14px; right: 14px; width: 32px; height: 32px; border: none; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; background: var(--pk-surface-alt); color: var(--pk-text-secondary); z-index: 2; transition: all .25s cubic-bezier(.4,0,.2,1); box-shadow: var(--pk-shadow-sm); } .pk-close-btn:hover { background: var(--pk-danger); color: #fff; transform: scale(1.1) rotate(90deg); box-shadow: 0 8px 20px rgba(239,68,68,.3); } .pk-panel-title { margin: 0 0 16px 0; color: var(--pk-text); text-align: center; font-size: clamp(18px, 3.5vw, 24px); font-weight: 700; display: flex; align-items: center; justify-content: center; gap: 10px; flex-wrap: wrap; } .pk-panel-title svg { flex-shrink: 0; } .pk-search-input { width: 100% !important; padding: 11px 18px !important; box-sizing: border-box !important; background: var(--pk-surface-alt) !important; border-radius: var(--pk-radius) !important; font-size: 15px !important; color: var(--pk-text) !important; outline: none !important; border: 2px solid transparent !important; margin-bottom: 14px !important; -webkit-appearance: none !important; appearance: none !important; font-weight: 500 !important; transition: all .3s ease !important; box-shadow: var(--pk-shadow-sm) !important; visibility: visible !important; opacity: 1 !important; display: block !important; } .pk-search-input:focus { border-color: var(--pk-primary); box-shadow: 0 0 0 3px rgba(99,102,241,.12), var(--pk-shadow); } .pk-search-input::placeholder { color: var(--pk-text-muted); } @media (max-width: 768px) { .pk-search-input { font-size: 15px; padding: 10px 16px; } } .pk-quick-engines { display: flex; flex-wrap: wrap; gap: 7px; margin-bottom: 18px; justify-content: center; } .pk-quick-engine-btn { padding: 6px 14px; border: 1px solid var(--pk-border); border-radius: 18px; background: var(--pk-surface-alt); color: var(--pk-text-secondary); font-size: 12px; font-weight: 500; cursor: pointer; transition: all .25s ease; display: inline-flex; align-items: center; gap: 5px; } .pk-quick-engine-btn:hover { background: var(--pk-primary); color: #fff; border-color: var(--pk-primary); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(99,102,241,.25); } .pk-quick-engine-btn svg { width: 14px; height: 14px; } .pk-nav-section { margin-top: 4px; } .pk-nav-title { color: var(--pk-text); margin-bottom: 12px; font-size: clamp(15px, 3vw, 18px); font-weight: 700; display: flex; align-items: center; justify-content: center; gap: 8px; text-align: center; flex-wrap: wrap; padding-bottom: 10px; border-bottom: 2px solid var(--pk-gradient); border-image: var(--pk-gradient) 1; } .pk-categories { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; margin-top: 8px; } @media (max-width: 480px) { .pk-categories { grid-template-columns: 1fr; gap: 10px; } } @media (max-width: 768px) { .pk-categories { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 10px; } } .pk-category { background: var(--pk-surface-alt); border-radius: var(--pk-radius); padding: 16px; border: 1px solid var(--pk-border); transition: all .3s ease; break-inside: avoid; } .pk-category:hover { transform: translateY(-3px); box-shadow: var(--pk-shadow-lg); border-color: var(--pk-primary-light); } .pk-category-title { margin: 0 0 12px 0; color: var(--pk-text); font-size: 14px; font-weight: 700; padding-bottom: 8px; border-bottom: 1px solid var(--pk-border); word-break: break-word; } .pk-sites { display: flex; flex-wrap: wrap; gap: 6px; } .pk-site-link { display: inline-block; padding: 6px 12px; background: var(--pk-surface); border: 1px solid var(--pk-border); border-radius: 8px; text-decoration: none; color: var(--pk-text-secondary); font-size: 12px; font-weight: 500; transition: all .25s ease; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; flex-shrink: 0; } .pk-site-link:hover { background: var(--pk-gradient); color: #fff; border-color: transparent; transform: translateY(-2px) scale(1.05); box-shadow: 0 4px 12px rgba(99,102,241,.3); } .pk-site-link:active { transform: translateY(0) scale(1); } /* ========== 管理面板 ========== */ #${CLASS_NAMES.MANAGEMENT_PANEL} { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); width: 92%; max-width: 860px; height: 90vh; max-height: 90vh; background: var(--pk-surface); border-radius: var(--pk-radius-xl); box-shadow: var(--pk-shadow-xl); z-index: 10000; display: none; overflow: hidden; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; flex-direction: column; box-sizing: border-box; border: 1px solid var(--pk-border); } @media (max-width: 768px) { #${CLASS_NAMES.MANAGEMENT_PANEL} { width: 96%; height: 94vh; border-radius: var(--pk-radius-lg); } } .pk-panel-header { background: var(--pk-gradient); color: #fff; padding: 24px 28px; position: relative; box-sizing: border-box; flex-shrink: 0; overflow: hidden; } .pk-panel-header::before { content: ''; position: absolute; top: -50%; right: -20%; width: 300px; height: 300px; background: rgba(255,255,255,.08); border-radius: 50%; } .pk-panel-header::after { content: ''; position: absolute; bottom: -30%; left: -10%; width: 200px; height: 200px; background: rgba(255,255,255,.05); border-radius: 50%; } .pk-panel-header h2 { margin: 0; font-size: clamp(18px, 4vw, 24px); font-weight: 700; display: flex; align-items: center; gap: 10px; position: relative; z-index: 1; } .pk-panel-header p { margin: 6px 0 0 0; opacity: .85; font-size: .85em; position: relative; z-index: 1; } .pk-unsaved-indicator { position: absolute; top: 16px; right: 24px; color: #fef3c7; font-size: .8em; font-weight: 600; display: none; align-items: center; gap: 6px; z-index: 2; background: rgba(245,158,11,.2); padding: 4px 12px; border-radius: 20px; backdrop-filter: blur(8px); } .pk-panel-content { flex: 1; overflow: hidden; display: flex; flex-direction: column; box-sizing: border-box; background: var(--pk-bg); } .pk-quick-actions { padding: 20px 24px; display: flex; gap: 12px; flex-wrap: wrap; justify-content: space-between; background: var(--pk-surface); border-bottom: 1px solid var(--pk-border); box-sizing: border-box; flex-shrink: 0; } .pk-action-group { display: flex; gap: 10px; flex-wrap: wrap; } .pk-action-btn { padding: 10px 18px; color: #fff; border: none; border-radius: 10px; cursor: pointer; font-size: 14px; font-weight: 600; min-width: 120px; transition: all .3s cubic-bezier(.4,0,.2,1); display: flex; align-items: center; gap: 6px; justify-content: center; box-shadow: var(--pk-shadow-sm); } .pk-action-btn:hover { transform: translateY(-2px); box-shadow: var(--pk-shadow-lg); } .pk-action-btn:active { transform: translateY(0); } .pk-action-btn.pk-btn-primary { background: var(--pk-primary); } .pk-action-btn.pk-btn-success { background: var(--pk-success); } .pk-action-btn.pk-btn-danger { background: var(--pk-danger); } .pk-action-btn.pk-btn-secondary { background: var(--pk-text-muted); } .pk-save-btn { padding: 10px 22px; background: var(--pk-text-muted); color: #fff; border: none; border-radius: 10px; cursor: pointer; font-size: 14px; font-weight: 600; display: flex; align-items: center; gap: 6px; transition: all .3s ease; opacity: .6; pointer-events: none; min-width: 130px; justify-content: center; } .pk-save-btn.pk-active { opacity: 1; pointer-events: auto; background: var(--pk-warning); box-shadow: 0 4px 12px rgba(245,158,11,.3); } .pk-list-section { flex: 1; overflow: auto; padding: 0 24px; box-sizing: border-box; display: flex; flex-direction: column; } .pk-list-title { color: var(--pk-text); margin: 18px 0; font-weight: 700; font-size: 16px; flex-shrink: 0; display: flex; align-items: center; gap: 10px; } .pk-engine-list { flex: 1; overflow-y: auto; overflow-x: hidden; display: grid; gap: 10px; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); padding-bottom: 16px; box-sizing: border-box; } @media (max-width: 480px) { .pk-engine-list { grid-template-columns: 1fr; } } .pk-engine-card { display: flex; align-items: center; padding: 12px 14px; background: var(--pk-surface); border: 2px solid var(--pk-border); border-radius: var(--pk-radius); transition: all .3s ease; cursor: grab; min-height: 60px; box-sizing: border-box; min-width: 0; overflow: hidden; } .pk-engine-card:hover { transform: translateY(-2px); box-shadow: var(--pk-shadow-lg); border-color: var(--pk-primary-light); } .pk-engine-card.pk-active { border-color: var(--pk-success); background: var(--pk-gradient-soft); } .pk-engine-card input[type="checkbox"] { appearance: auto !important; -webkit-appearance: auto !important; -moz-appearance: auto !important; width: 18px !important; height: 18px !important; margin: 0 10px 0 0 !important; transform: scale(1.2) !important; accent-color: var(--pk-primary) !important; cursor: pointer !important; flex-shrink: 0 !important; visibility: visible !important; opacity: 1 !important; display: inline-block !important; box-sizing: content-box !important; } .pk-engine-icon { width: 36px; height: 24px; background-size: contain; background-repeat: no-repeat; background-position: center; margin-right: 10px; border: 1px solid var(--pk-border); border-radius: 5px; flex-shrink: 0; } .pk-engine-info { flex-grow: 1; min-width: 0; overflow: hidden; } .pk-engine-name { font-weight: 700; color: var(--pk-text); margin-bottom: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 14px; } .pk-engine-url { font-size: 12px; color: var(--pk-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .pk-engine-actions { display: flex; gap: 6px; flex-shrink: 0; margin-left: 8px; } .pk-delete-btn { padding: 8px; border: none; background: var(--pk-danger); color: #fff; border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all .25s ease; } .pk-delete-btn:hover { transform: scale(1.1); box-shadow: 0 4px 12px rgba(239,68,68,.3); } /* ========== 添加表单 ========== */ .pk-add-form { display: none; background: var(--pk-surface-alt); padding: 24px; border-radius: var(--pk-radius); margin: 12px 0; box-sizing: border-box; flex-shrink: 0; border: 1px solid var(--pk-border); } .pk-form-title { color: var(--pk-text); margin-bottom: 18px; font-weight: 700; font-size: 16px; display: flex; align-items: center; gap: 10px; } .pk-form-grid { display: grid; gap: 16px; grid-template-columns: 1fr 1fr; } @media (max-width: 600px) { .pk-form-grid { grid-template-columns: 1fr; } } .pk-field-full { grid-column: 1 / -1; } .pk-field label { display: block; margin-bottom: 6px; font-weight: 600; color: var(--pk-text-secondary); font-size: 13px; } .pk-field input, .pk-field select { width: 100% !important; padding: 12px 14px !important; border: 2px solid var(--pk-border) !important; border-radius: 10px !important; font-size: 14px !important; color: var(--pk-text) !important; background: var(--pk-surface) !important; box-sizing: border-box !important; transition: all .25s ease !important; -webkit-appearance: none !important; appearance: none !important; visibility: visible !important; opacity: 1 !important; } .pk-field input:focus, .pk-field select:focus { outline: none !important; border-color: var(--pk-primary) !important; box-shadow: 0 0 0 4px rgba(99,102,241,.12) !important; } .pk-icon-grid { display: grid; grid-template-columns: 1fr 2fr 1fr; gap: 12px; align-items: end; } @media (max-width: 600px) { .pk-icon-grid { grid-template-columns: 1fr; } } .pk-preview-btn { width: 100%; padding: 12px; background: var(--pk-primary); color: #fff; border: none; border-radius: 10px; cursor: pointer; display: flex; align-items: center; justify-content: center; gap: 6px; font-weight: 600; font-size: 14px; transition: all .25s ease; } .pk-preview-btn:hover { background: var(--pk-primary-dark); transform: translateY(-1px); box-shadow: 0 4px 12px rgba(99,102,241,.3); } .pk-preview-area { margin-top: 16px; text-align: center; } .pk-preview-area label { display: block; margin-bottom: 10px; font-weight: 600; color: var(--pk-text-secondary); font-size: 13px; } .pk-icon-preview { width: 88px; height: 55px; border: 2px dashed var(--pk-border); border-radius: 8px; margin: 0 auto; display: flex; justify-content: center; align-items: center; overflow: hidden; background: var(--pk-surface); } .pk-form-actions { grid-column: 1 / -1; display: flex; gap: 12px; margin-top: 20px; } /* ========== 面板底部 ========== */ .pk-panel-footer { background: var(--pk-surface); padding: 16px 24px; border-top: 1px solid var(--pk-border); display: flex; justify-content: space-between; align-items: center; box-sizing: border-box; flex-shrink: 0; } .pk-selected-count { color: var(--pk-text-secondary); font-size: .9em; display: flex; align-items: center; gap: 6px; font-weight: 500; } /* ========== 滚动条美化 ========== */ .pk-list-section::-webkit-scrollbar, .pk-engine-list::-webkit-scrollbar, .pk-overlay-scroll::-webkit-scrollbar { width: 6px; } .pk-list-section::-webkit-scrollbar-track, .pk-engine-list::-webkit-scrollbar-track, .pk-overlay-scroll::-webkit-scrollbar-track { background: transparent; } .pk-list-section::-webkit-scrollbar-thumb, .pk-engine-list::-webkit-scrollbar-thumb, .pk-overlay-scroll::-webkit-scrollbar-thumb { background: var(--pk-border); border-radius: 3px; } .pk-list-section::-webkit-scrollbar-thumb:hover, .pk-engine-list::-webkit-scrollbar-thumb:hover, .pk-overlay-scroll::-webkit-scrollbar-thumb:hover { background: var(--pk-text-muted); } `; 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(() => { debounceUtils.debounce('input_monitor_setup', () => { document.querySelectorAll(`${DEFAULT_CONFIG.MONITORED_INPUT_SELECTOR}:not([data-monitored])`).forEach(setupInputMonitoring); }, 500); }); 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; // Prevent duplicate event binding when called multiple times (e.g. after sort mode switch) if (container._dragSortBound) return; container._dragSortBound = true; const buttons = container.querySelectorAll(`.${CLASS_NAMES.ENGINE_BUTTON}`); let draggingButton = null; let dropIndicator = null; let currentTarget = null; // Ensure drop indicator element exists const getDropIndicator = () => { if (!dropIndicator) { dropIndicator = document.createElement('div'); dropIndicator.className = 'pk-drop-indicator'; } return dropIndicator; }; // Calculate which button the cursor is hovering over and where to insert const getInsertTarget = (clientX) => { const btns = Array.from(container.querySelectorAll(`.${CLASS_NAMES.ENGINE_BUTTON}:not(.${CLASS_NAMES.DRAGGING})`)); for (const btn of btns) { const rect = btn.getBoundingClientRect(); const midX = rect.left + rect.width / 2; if (clientX < midX) { return { target: btn, position: 'before' }; } } // Cursor is past the last button if (btns.length > 0) { return { target: btns[btns.length - 1], position: 'after' }; } return null; }; // Update visual indicator during drag const updateIndicator = (clientX) => { // Remove previous highlight container.querySelectorAll(`.${CLASS_NAMES.DRAG_OVER}`).forEach(el => el.classList.remove(CLASS_NAMES.DRAG_OVER)); const indicator = getDropIndicator(); if (indicator.parentNode) indicator.remove(); const result = getInsertTarget(clientX); if (!result) return; result.target.classList.add(CLASS_NAMES.DRAG_OVER); currentTarget = result; if (result.position === 'before') { container.insertBefore(indicator, result.target); } else { container.insertBefore(indicator, result.target.nextSibling); } }; buttons.forEach(button => { button.draggable = true; button.addEventListener('dragstart', (e) => { draggingButton = button; button.classList.add(CLASS_NAMES.DRAGGING); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', button.getAttribute('url') || ''); // Haptic feedback when drag starts if (navigator.vibrate) navigator.vibrate(10); try { const dragImg = button.cloneNode(true); dragImg.style.cssText = 'position:fixed; top:-9999px; left:-9999px; opacity:0.85; transform:none; border:2px solid #6366f1; border-radius:8px;'; document.body.appendChild(dragImg); e.dataTransfer.setDragImage(dragImg, 28, 16); setTimeout(() => dragImg.remove(), 0); } catch (_) {} }); button.addEventListener('dragend', () => { button.classList.remove(CLASS_NAMES.DRAGGING); container.querySelectorAll(`.${CLASS_NAMES.DRAG_OVER}`).forEach(el => el.classList.remove(CLASS_NAMES.DRAG_OVER)); const indicator = getDropIndicator(); if (indicator.parentNode) indicator.remove(); draggingButton = null; currentTarget = null; utils.saveButtonOrder(); }); }); // Container-level drag tracking — works across buttons AND gaps container.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (draggingButton) { updateIndicator(e.clientX); } }); container.addEventListener('drop', (e) => { e.preventDefault(); if (!draggingButton || !currentTarget) { const indicator = getDropIndicator(); if (indicator.parentNode) indicator.remove(); return; } const { target, position } = currentTarget; if (target === draggingButton) return; if (position === 'before') { container.insertBefore(draggingButton, target); } else { container.insertBefore(draggingButton, target.nextSibling); } if (navigator.vibrate) navigator.vibrate(15); // Note: saveButtonOrder is called in dragend, no need to markUnsavedChanges here // Cleanup container.querySelectorAll(`.${CLASS_NAMES.DRAG_OVER}`).forEach(el => el.classList.remove(CLASS_NAMES.DRAG_OVER)); const indicator = getDropIndicator(); if (indicator.parentNode) indicator.remove(); currentTarget = null; }); // Also support dragleave on container to clean up when leaving entirely container.addEventListener('dragleave', (e) => { // Only clean up if leaving the container itself if (!container.contains(e.relatedTarget)) { container.querySelectorAll(`.${CLASS_NAMES.DRAG_OVER}`).forEach(el => el.classList.remove(CLASS_NAMES.DRAG_OVER)); const indicator = getDropIndicator(); if (indicator.parentNode) indicator.remove(); } }); }, 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(); } }; // ===== 搜索遮罩层模块 ===== 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; const scrollContainer = document.createElement("div"); scrollContainer.className = 'pk-overlay-scroll'; const searchContainer = document.createElement("div"); searchContainer.className = 'pk-search-card'; // Close button const closeBtn = document.createElement("button"); closeBtn.className = 'pk-close-btn'; closeBtn.innerHTML = utils.createInlineSVG('times'); closeBtn.setAttribute('aria-label', '关闭搜索'); closeBtn.addEventListener('click', () => this.hideSearchOverlay()); // Title const title = document.createElement("h2"); title.className = 'pk-panel-title'; title.innerHTML = utils.createInlineSVG('search') + ' 快捷搜索 (Alt+S)'; // Search input const searchInput = document.createElement("input"); searchInput.type = "text"; searchInput.placeholder = "输入关键词或网址..."; searchInput.id = "overlay-search-input"; searchInput.className = 'pk-search-input'; searchInput.setAttribute('autocomplete', 'off'); searchInput.setAttribute('autocorrect', 'off'); searchInput.setAttribute('autocapitalize', 'off'); searchInput.setAttribute('spellcheck', 'false'); // Quick engine buttons const quickEnginesContainer = document.createElement("div"); quickEnginesContainer.className = 'pk-quick-engines'; const showList = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK).split('-').slice(0, 8); showList.forEach(showMark => { const engine = appState.searchUrlMap.find(e => e.mark === showMark); if (!engine) return; const btn = document.createElement("button"); btn.className = 'pk-quick-engine-btn'; btn.innerHTML = utils.createInlineSVG('paper-plane') + ' ' + engine.name; btn.addEventListener('click', () => { const query = searchInput.value.trim(); if (!query) { searchInput.focus(); return; } const searchUrl = engine.searchUrl.replace('{keyword}', encodeURIComponent(query)); window.open(searchUrl, '_blank'); }); quickEnginesContainer.appendChild(btn); }); // Navigation section const navigationSection = document.createElement("div"); navigationSection.className = 'pk-nav-section'; const navTitle = document.createElement("h3"); navTitle.className = 'pk-nav-title'; navTitle.innerHTML = utils.createInlineSVG('globe') + ' 常用网站导航'; navigationSection.appendChild(navTitle); const categoriesContainer = document.createElement("div"); categoriesContainer.className = 'pk-categories'; const websiteCategories = [ { title: "🔧 逆向论坛区", sites: [ { name: "MT论坛", url: "https://bbs.binmt.cc" }, { name: "吾爱破解", url: "https://www.52pojie.cn" }, { name: "看雪论坛", url: "https://bbs.kanxue.com" }, { name: "飘云阁", url: "https://www.chinapyg.com" }, { name: "绿盟科技社区", url: "https://www.nsfocus.net" }, { name: "先知社区", url: "https://xz.aliyun.com" }, { name: "安全客", url: "https://www.anquanke.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.appinn.com" }, { name: "异星软件", url: "https://www.yxdown.com" }, { name: "RuTracker", url: "https://rutracker.org" } ] }, { title: "🤖 AI工具", sites: [ { name: "ChatGPT", url: "https://chatgpt.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: "Kimi", url: "https://kimi.moonshot.cn" }, { name: "通义千问", url: "https://tongyi.aliyun.com" }, { name: "Midjourney", url: "https://www.midjourney.com" }, { name: "Stable Diffusion", url: "https://stability.ai" }, { name: "Groq", url: "https://groq.com" }, { name: "Poe", url: "https://poe.com" } ] }, { title: "🎬 影视区", sites: [ { 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://www.ntdm9.com/" }, { name: "AGE动漫", url: "https://www.agemys.org/" }, { name: "樱花动漫", url: "https://www.yhdmp.cc" }, { name: "腾讯视频", url: "https://v.qq.com" }, { name: "爱奇艺", url: "https://www.iqiyi.com" }, { name: "芒果TV", url: "https://www.mgtv.com" }, { name: "优酷", url: "https://www.youku.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.feishu.cn" }, { name: "Figma", url: "https://www.figma.com" }, { name: "Excalidraw", url: "https://excalidraw.com" }, { name: "Photopea", url: "https://www.photopea.com" }, { name: "TempMail", url: "https://temp-mail.org" } ] }, { 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: "Library Genesis", url: "https://libgen.is" }, { name: "Z-Library", url: "https://z-lib.org" }, { name: "Sci-Hub", url: "https://sci-hub.se" }, { name: "OALib", url: "https://www.oalib.com" } ] }, { 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://www.goofish.com" }, { name: "盒马鲜生", url: "https://www.hemaapp.com" }, { name: "当当网", url: "https://www.dangdang.com" }, { name: "1688", url: "https://www.1688.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://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: "Bandcamp", url: "https://bandcamp.com" }, { name: "SoundCloud", url: "https://soundcloud.com" } ] }, { 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://gitlab.com" }, { name: "Stack Overflow", url: "https://stackoverflow.com" }, { name: "阿里开发者社区", url: "https://developer.aliyun.com" }, { name: "腾讯云开发者社区", url: "https://cloud.tencent.com/developer" }, { name: "Gitee", url: "https://gitee.com" } ] }, { title: "🎮 游戏区", sites: [ { name: "Steam", url: "https://store.steampowered.com" }, { name: "Epic Games", url: "https://store.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: "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: "先知社区", url: "https://xz.aliyun.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: "Kali Linux", url: "https://www.kali.org" }, { name: "OWASP", url: "https://owasp.org" }, { name: "SANS Institute", url: "https://www.sans.org" }, { name: "Vulhub", url: "https://vulhub.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://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: "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" }, { name: "GitHub Gist", url: "https://gist.github.com" } ] }, { 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.heywhale.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" } ] } ]; const categoryElements = []; websiteCategories.forEach(category => { const categoryElement = document.createElement("div"); categoryElement.className = 'pk-category'; const categoryTitle = document.createElement("h4"); categoryTitle.className = 'pk-category-title'; categoryTitle.textContent = category.title; const sitesContainer = document.createElement("div"); sitesContainer.className = 'pk-sites'; category.sites.forEach(site => { const siteLink = document.createElement("a"); siteLink.className = 'pk-site-link'; siteLink.textContent = site.name; siteLink.href = site.url; siteLink.target = "_blank"; siteLink.rel = "noopener noreferrer"; sitesContainer.appendChild(siteLink); }); categoryElement.appendChild(categoryTitle); categoryElement.appendChild(sitesContainer); categoriesContainer.appendChild(categoryElement); categoryElements.push({ element: categoryElement, title: category.title, sites: category.sites }); }); navigationSection.appendChild(categoriesContainer); // Assemble searchContainer.appendChild(closeBtn); searchContainer.appendChild(title); searchContainer.appendChild(searchInput); searchContainer.appendChild(quickEnginesContainer); searchContainer.appendChild(navigationSection); scrollContainer.appendChild(searchContainer); overlay.appendChild(scrollContainer); // Events searchInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') this.performOverlaySearch(); }); overlay.addEventListener('click', (e) => { if (e.target === overlay || e.target === scrollContainer) 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); this._outsideClickHandler = (e) => this.handleClickOutsideContextMenu(e); document.addEventListener('click', this._outsideClickHandler); }, removeSortContextMenu() { const contextMenu = document.getElementById('sort-context-menu'); if (contextMenu) contextMenu.remove(); if (this._outsideClickHandler) { document.removeEventListener('click', this._outsideClickHandler); this._outsideClickHandler = null; } }, 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)); // Re-enable draggable in case we switched back from smart mode buttons.forEach(btn => { btn.draggable = true; }); 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; // Secondary sort: if same usage count, preserve original order if (aCount === bCount) { const order = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK).split('-'); return order.indexOf(aMark) - order.indexOf(bMark); } 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.className = 'pk-action-btn'; // Map colors to semantic classes const colorMap = { '#3498db': 'pk-btn-primary', '#27ae60': 'pk-btn-success', '#e74c3c': 'pk-btn-danger', '#95a5a6': 'pk-btn-secondary' }; button.classList.add(colorMap[color] || 'pk-btn-primary'); 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, '_'); const escapedDomain = cleanDomain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return { name: name, searchUrl: searchUrl, searchkeyName: keyParams, matchUrl: new RegExp(`.*${escapedDomain}.*`), 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 = document.querySelector('.pk-list-title'); if (!formSection) return; if (show) { formSection.style.display = "block"; if (engineList) engineList.style.display = "none"; if (listTitle) listTitle.style.display = "none"; ["engine-name", "engine-mark", "engine-url", "engine-keys", "icon-input"].forEach(id => { const el = document.getElementById(id); if (el) el.value = ""; }); const preview = document.getElementById("icon-preview"); if (preview) preview.innerHTML = ""; } else { formSection.style.display = "none"; if (engineList) engineList.style.display = "grid"; if (listTitle) listTitle.style.display = "flex"; } }, 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"; 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 hostname = new URL(url).hostname; const escapedHostname = hostname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const newEngine = { name, searchUrl: url, searchkeyName: keys, matchUrl: new RegExp(`.*${escapedHostname}.*`), 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 = 'pk-engine-card' + (activeMarks.includes(engine.mark) ? ' pk-active' : ''); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.dataset.mark = engine.mark; checkbox.checked = activeMarks.includes(engine.mark); checkbox.addEventListener("change", () => { utils.updateSelectedCount(); utils.markUnsavedChanges(); if (checkbox.checked) engineCard.classList.add('pk-active'); else engineCard.classList.remove('pk-active'); }); const iconPreview = document.createElement("div"); iconPreview.className = 'pk-engine-icon'; iconPreview.style.backgroundImage = `url('data:image/svg+xml;utf8,${encodeURIComponent(engine.svgCode)}')`; const infoContainer = document.createElement("div"); infoContainer.className = 'pk-engine-info'; const name = document.createElement("div"); name.className = 'pk-engine-name'; name.textContent = engine.name; const url = document.createElement("div"); url.className = 'pk-engine-url'; url.textContent = engine.searchUrl; infoContainer.appendChild(name); infoContainer.appendChild(url); const actions = document.createElement("div"); actions.className = 'pk-engine-actions'; if (engine.custom) { const deleteBtn = document.createElement("button"); deleteBtn.className = 'pk-delete-btn'; deleteBtn.innerHTML = utils.createInlineSVG('trash', 'white'); deleteBtn.title = "删除"; 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(); } }); actions.appendChild(deleteBtn); } 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 checkedMarks = []; const uncheckedMarks = []; checkboxes.forEach(checkbox => { if (checkbox.checked) checkedMarks.push(checkbox.dataset.mark); else uncheckedMarks.push(checkbox.dataset.mark); }); if (checkedMarks.length === 0) { alert("⚠️ 请至少选择一个搜索引擎"); return; } // Preserve existing sort order: keep previously-saved marks first (in their saved order), // then append newly-enabled marks that weren't in the saved list before const savedOrder = GM_getValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, DEFAULT_CONFIG.PUNK_DEFAULT_MARK).split('-'); const orderedActive = savedOrder.filter(mark => checkedMarks.includes(mark)); const newActive = checkedMarks.filter(mark => !savedOrder.includes(mark)); const finalOrder = [...orderedActive, ...newActive]; GM_setValue(STORAGE_KEYS.PUNK_SETUP_SEARCH, finalOrder.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; // ========== Header ========== const header = document.createElement("div"); header.className = 'pk-panel-header'; const title = document.createElement("h2"); title.innerHTML = utils.createInlineSVG('cog', 'white') + ' 搜索引擎管理中心'; const subtitle = document.createElement("p"); subtitle.textContent = "管理您的搜索快捷方式 · 拖拽排序 · 勾选启用"; const unsavedIndicator = document.createElement("div"); unsavedIndicator.id = "unsaved-indicator"; unsavedIndicator.className = 'pk-unsaved-indicator'; unsavedIndicator.innerHTML = utils.createInlineSVG('circle', '#fef3c7') + ' 有未保存的更改'; header.appendChild(title); header.appendChild(subtitle); header.appendChild(unsavedIndicator); panel.appendChild(header); // ========== Content ========== const content = document.createElement("div"); content.className = 'pk-panel-content'; // --- Quick Actions --- const quickActions = document.createElement("div"); quickActions.className = 'pk-quick-actions'; const leftActionGroup = document.createElement("div"); leftActionGroup.className = 'pk-action-group'; 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.className = 'pk-action-group'; const saveBtn = document.createElement("button"); saveBtn.id = "panel-save-btn"; saveBtn.className = 'pk-save-btn'; saveBtn.innerHTML = utils.createInlineSVG('save') + ' 保存设置'; saveBtn.title = "保存当前设置"; const resetBtn = this.createActionButton(utils.createInlineSVG('undo') + ' 恢复默认', "#e74c3c", "恢复默认搜索引擎设置"); rightActionGroup.appendChild(saveBtn); rightActionGroup.appendChild(resetBtn); quickActions.appendChild(leftActionGroup); quickActions.appendChild(rightActionGroup); content.appendChild(quickActions); // --- List Section --- const listSection = document.createElement("div"); listSection.className = 'pk-list-section'; const listTitle = document.createElement("h3"); listTitle.className = 'pk-list-title'; listTitle.innerHTML = utils.createInlineSVG('list') + ' 已配置的搜索引擎'; const engineList = document.createElement("div"); engineList.id = "engine-management-list"; engineList.className = 'pk-engine-list'; listSection.appendChild(listTitle); listSection.appendChild(engineList); // --- Add Form --- const formSection = document.createElement("div"); formSection.id = "add-engine-form"; formSection.className = 'pk-add-form'; const formTitle = document.createElement("h3"); formTitle.className = 'pk-form-title'; formTitle.innerHTML = utils.createInlineSVG('magic') + ' 添加新搜索引擎'; formSection.appendChild(formTitle); const form = document.createElement("div"); form.className = 'pk-form-grid'; const fields = [ { label: "引擎名称", placeholder: "例如: Google", id: "engine-name", required: true }, { label: "唯一标识", placeholder: "例如: google", id: "engine-mark", required: true }, { label: "搜索URL", placeholder: "使用 {keyword} 作为占位符", id: "engine-url", required: true, fullWidth: true }, { label: "关键词参数", placeholder: "例如: q,query,search", id: "engine-keys", required: true, fullWidth: true } ]; fields.forEach(field => { const container = document.createElement("div"); container.className = 'pk-field' + (field.fullWidth ? ' pk-field-full' : ''); const label = document.createElement("label"); label.textContent = field.label; label.setAttribute('for', field.id); const input = document.createElement("input"); input.type = "text"; input.placeholder = field.placeholder; input.id = field.id; input.required = field.required; container.appendChild(label); container.appendChild(input); form.appendChild(container); }); // Icon settings const iconContainer = document.createElement("div"); iconContainer.className = 'pk-field-full'; const iconTitle = document.createElement("h4"); iconTitle.className = 'pk-form-title'; iconTitle.style.fontSize = '14px'; iconTitle.innerHTML = utils.createInlineSVG('palette') + ' 图标设置'; iconContainer.appendChild(iconTitle); const iconGrid = document.createElement("div"); iconGrid.className = 'pk-icon-grid'; // Type select const typeGroup = document.createElement("div"); typeGroup.className = 'pk-field'; const typeLabel = document.createElement("label"); typeLabel.textContent = "图标类型"; typeGroup.appendChild(typeLabel); const iconTypeSelect = document.createElement("select"); iconTypeSelect.id = "icon-type"; ["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); // Input const inputGroup = document.createElement("div"); inputGroup.className = 'pk-field'; const inputLabel = document.createElement("label"); inputLabel.textContent = "图标内容"; inputGroup.appendChild(inputLabel); const iconInput = document.createElement("input"); iconInput.type = "text"; iconInput.id = "icon-input"; iconInput.placeholder = "SVG代码、图片URL、文字或表情符号"; inputGroup.appendChild(iconInput); // Preview button const previewGroup = document.createElement("div"); previewGroup.className = 'pk-field'; const previewButton = document.createElement("button"); previewButton.innerHTML = utils.createInlineSVG('eye') + ' 预览图标'; previewButton.className = 'pk-preview-btn'; previewButton.id = "preview-icon"; previewGroup.appendChild(previewButton); iconGrid.appendChild(typeGroup); iconGrid.appendChild(inputGroup); iconGrid.appendChild(previewGroup); iconContainer.appendChild(iconGrid); // Preview area const previewContainer = document.createElement("div"); previewContainer.className = 'pk-preview-area pk-field-full'; const previewLabel = document.createElement("label"); previewLabel.textContent = "图标预览 (推荐比例 8:5)"; const iconPreview = document.createElement("div"); iconPreview.id = "icon-preview"; iconPreview.className = 'pk-icon-preview'; previewContainer.appendChild(previewLabel); previewContainer.appendChild(iconPreview); iconContainer.appendChild(previewContainer); form.appendChild(iconContainer); // Form actions const formActions = document.createElement("div"); formActions.className = 'pk-form-actions'; const saveFormBtn = this.createActionButton(utils.createInlineSVG('save') + ' 保存引擎', "#27ae60", ""); const cancelFormBtn = this.createActionButton(utils.createInlineSVG('times') + ' 取消', "#95a5a6", ""); formActions.appendChild(saveFormBtn); formActions.appendChild(cancelFormBtn); form.appendChild(formActions); formSection.appendChild(form); listSection.appendChild(formSection); content.appendChild(listSection); panel.appendChild(content); // ========== Footer ========== const footer = document.createElement("div"); footer.className = 'pk-panel-footer'; const selectedCount = document.createElement("span"); selectedCount.id = "selected-count"; selectedCount.className = 'pk-selected-count'; selectedCount.innerHTML = utils.createInlineSVG('check-circle') + ' 已选择 0 个引擎'; const footerActions = document.createElement("div"); footerActions.className = 'pk-action-group'; const closeBtn = this.createActionButton(utils.createInlineSVG('times') + ' 关闭', "#95a5a6", ""); footerActions.appendChild(closeBtn); footer.appendChild(selectedCount); footer.appendChild(footerActions); panel.appendChild(footer); // ========== Events ========== 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 = "flex"; // 应用焦点陷阱 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. 重置应用状态 appState.scriptLoaded = false; appState.containerAdded = false; appState.hamburgerMenuOpen = false; appState.searchOverlayVisible = false; // 4. 重新初始化 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); } }; // 启动应用 appInitializer.initPageEventListeners();