谷歌换搜

在 Google 搜索页面优化搜索栏,添加多站点搜索切换功能,导航栏悬浮且紧凑、可拖动,可自定义站点,支持与GitHub Gist同步,站点编辑框可调整大小并记忆。

// ==UserScript==
// @name         谷歌换搜
// @version      1.0.0
// @namespace    https://sleazyfork.org/zh-CN/users/1461640-%E6%98%9F%E5%AE%BF%E8%80%81%E9%AD%94
// @author       星宿老魔
// @description  在 Google 搜索页面优化搜索栏,添加多站点搜索切换功能,导航栏悬浮且紧凑、可拖动,可自定义站点,支持与GitHub Gist同步,站点编辑框可调整大小并记忆。
// @match        https://www.google.com/search?q=*site*
// @match        https://www.google.com.hk/search?q=*site*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// @license      MIT
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// ==/UserScript==

(function(){"use strict";const s={DEBUG_MODE:!1,STORAGE:{GM_GITHUB_TOKEN_KEY:"googleSearchMultiSite_github_token",GM_GIST_ID_KEY:"googleSearchMultiSite_gist_id",LOCALSTORAGE_SITES_KEY:"customSiteRowsText",LOCALSTORAGE_NAVBAR_POSITION_KEY:"navBarPosition",LOCALSTORAGE_TEXTAREA_WIDTH_KEY:"googleSearchMultiSite_siteConfigTextareaWidth",LOCALSTORAGE_TEXTAREA_HEIGHT_KEY:"googleSearchMultiSite_siteConfigTextareaHeight"},GIST:{FILENAME:"googleSearchMultiSite_sites-config.txt",DESCRIPTION:"谷歌搜图多站点配置"},DEFAULT_SITES:["暗香,anxiangge.cc 2nt,mm2211.blog.2nt.com 2048,hjd2048.com","Intporn,forum.intporn.com EC,eroticity.net SPS,sexpicturespass.com planetsuzy,planetsuzy.org"].join(`
`),SELECTORS:{SEARCH_BOX:"textarea[name='q']",SUBMIT_BUTTON:"button[type='submit']"},UI:{NAVBAR:{DEFAULT_POSITION:{top:"100px",left:"10px"},Z_INDEX:"10000"},DIALOG:{Z_INDEX:"99999",SETTINGS_Z_INDEX:"10000"},NOTIFICATION:{Z_INDEX:"10001"}}};function a(c,t="info",i=3e3){const o="userscript-notification-"+Date.now(),e=document.createElement("div");switch(e.id=o,e.textContent=c,Object.assign(e.style,{position:"fixed",bottom:"20px",left:"50%",transform:"translateX(-50%)",padding:"10px 20px",borderRadius:"5px",color:"white",zIndex:s.UI.NOTIFICATION.Z_INDEX,boxShadow:"0 2px 10px rgba(0,0,0,0.2)",opacity:"0",transition:"opacity 0.5s ease-in-out"}),t){case"success":e.style.backgroundColor="#4CAF50";break;case"error":e.style.backgroundColor="#F44336";break;case"warning":e.style.backgroundColor="#FF9800";break;default:e.style.backgroundColor="#2196F3";break}return document.body.appendChild(e),setTimeout(()=>{e.style.opacity="1"},10),i>0&&setTimeout(()=>{e.style.opacity="0",setTimeout(()=>{e.remove()},500)},i),e}function E(c){return c.split(`
`).map(t=>t.trim().split(/\s+/).filter(Boolean).map(i=>{const[o,e]=i.split(",");return{name:o||"",url:e||""}}).filter(i=>i.name&&i.url)).filter(t=>t.length>0)}function f(){return(new URLSearchParams(window.location.search).get("q")||"").replace(/site:[^\s]+/g,"").trim()}function S(){return document.querySelector(s.SELECTORS.SEARCH_BOX)}const g=class g{static init(){S()&&(this.createNavBar(),this.setupDragEvents())}static createNavBar(){this.navBar||(this.navBar=document.createElement("div"),this.applyNavBarStyles(),this.loadPosition(),this.createSiteButtons(),document.body.appendChild(this.navBar))}static applyNavBarStyles(){this.navBar&&Object.assign(this.navBar.style,{position:"fixed",top:s.UI.NAVBAR.DEFAULT_POSITION.top,left:s.UI.NAVBAR.DEFAULT_POSITION.left,zIndex:s.UI.NAVBAR.Z_INDEX,background:"#f8f8f8",border:"1px solid #ccc",padding:"3px 10px",borderRadius:"8px",boxShadow:"0 4px 8px rgba(0, 0, 0, 0.2)",display:"flex",flexDirection:"column",gap:"5px",cursor:"move",transition:"all 0.3s ease"})}static loadPosition(){if(!this.navBar)return;const t=localStorage.getItem(s.STORAGE.LOCALSTORAGE_NAVBAR_POSITION_KEY);if(t)try{const i=JSON.parse(t);this.navBar.style.top=i.top,this.navBar.style.left=i.left}catch{}}static savePosition(){if(!this.navBar)return;const t={top:this.navBar.style.top,left:this.navBar.style.left};localStorage.setItem(s.STORAGE.LOCALSTORAGE_NAVBAR_POSITION_KEY,JSON.stringify(t))}static createSiteButtons(){if(!this.navBar)return;const t=localStorage.getItem(s.STORAGE.LOCALSTORAGE_SITES_KEY)||s.DEFAULT_SITES;E(t).forEach(o=>{const e=document.createElement("div");e.style.display="flex",e.style.gap="5px",o.forEach(({name:n,url:l})=>{const r=document.createElement("button");r.textContent=n,this.applyButtonStyles(r),r.addEventListener("click",()=>{this.performSearch(l)}),e.appendChild(r)}),this.navBar.appendChild(e)})}static applyButtonStyles(t){Object.assign(t.style,{padding:"2px 6px",cursor:"pointer",border:"1px solid #ccc",background:"#f8f8f8",borderRadius:"5px",fontSize:"10px"})}static performSearch(t){const i=S();if(!i)return;const o=f();i.value=`${o} site:${t}`;const e=document.querySelector(s.SELECTORS.SUBMIT_BUTTON);e&&e.click()}static setupDragEvents(){this.navBar&&(this.navBar.addEventListener("mousedown",t=>{this.isDragging=!0,this.offsetX=t.clientX-this.navBar.offsetLeft,this.offsetY=t.clientY-this.navBar.offsetTop,this.navBar.style.transition="none"}),window.addEventListener("mousemove",t=>{this.isDragging&&this.navBar&&(this.navBar.style.left=`${t.clientX-this.offsetX}px`,this.navBar.style.top=`${t.clientY-this.offsetY}px`)}),window.addEventListener("mouseup",()=>{this.isDragging&&this.navBar&&(this.isDragging=!1,this.savePosition(),this.navBar.style.transition="all 0.3s ease")}))}};g.navBar=null,g.isDragging=!1,g.offsetX=0,g.offsetY=0;let h=g;class T{static show(){const t=document.getElementById("googleSearchMultiSite-settings-dialog");t&&t.remove();const i=document.createElement("div");i.id="googleSearchMultiSite-settings-dialog";const o=document.createElement("div");o.id="googleSearchMultiSite-settings-dialog-content",o.innerHTML=`
      <button id="googleSearchMultiSite-settings-close-btn" title="关闭">&times;</button>
      <h3>Gist 同步参数配置</h3>
      <div>
        <label for="gist_token_input_gsms">GitHub 个人访问令牌 (Token):</label>
        <input type="password" id="gist_token_input_gsms" value="${GM_getValue(s.STORAGE.GM_GITHUB_TOKEN_KEY,"")}" placeholder="例如 ghp_xxxxxxxxxxxxxxxxx">
        <small>Token 用于授权访问您的Gist。需要 Gist 读写权限。</small>
      </div>
      <div>
        <label for="gist_id_input_gsms">Gist ID:</label>
        <input type="text" id="gist_id_input_gsms" value="${GM_getValue(s.STORAGE.GM_GIST_ID_KEY,"")}" placeholder="例如 123abc456def7890">
        <small>Gist ID 是备份用Gist的标识。若为空,首次上传时将自动创建并保存。</small>
      </div>
      <div class="rw-dialog-buttons">
        <button id="settings_cancel_btn_gsms" class="rw-cancel-btn">取消</button>
        <button id="settings_save_btn_gsms" class="rw-save-btn">保存配置</button>
      </div>
    `,i.appendChild(o),document.body.appendChild(i),this.applyStyles();const e=d=>{d.key==="Escape"&&n()},n=()=>{document.removeEventListener("keydown",e),i.remove()},l=()=>{const d=document.getElementById("gist_token_input_gsms").value.trim(),u=document.getElementById("gist_id_input_gsms").value.trim();GM_setValue(s.STORAGE.GM_GITHUB_TOKEN_KEY,d),GM_setValue(s.STORAGE.GM_GIST_ID_KEY,u);let p="Gist参数已保存!";!d&&!u?p="Gist参数已清空。":d?u||(p="Gist ID已清空, Token已保存。"):p="Token已清空, Gist ID已保存。",a(p,"success"),n()},r=()=>{n(),a("参数设置已取消。","info")};document.getElementById("settings_save_btn_gsms")?.addEventListener("click",l),document.getElementById("settings_cancel_btn_gsms")?.addEventListener("click",r),document.getElementById("googleSearchMultiSite-settings-close-btn")?.addEventListener("click",r),document.addEventListener("keydown",e)}static applyStyles(){GM_addStyle(`
      #googleSearchMultiSite-settings-dialog {
        position: fixed; top: 0; left: 0; width: 100%; height: 100%;
        background-color: rgba(0,0,0,0.6); z-index: ${s.UI.DIALOG.SETTINGS_Z_INDEX};
        display: flex; justify-content: center; align-items: center; font-family: sans-serif;
      }
      #googleSearchMultiSite-settings-dialog-content {
        background: white; padding: 25px; border-radius: 8px;
        box-shadow: 0 5px 20px rgba(0,0,0,0.3); width: 400px; max-width: 90%;
        position: relative;
      }
      #googleSearchMultiSite-settings-dialog-content h3 { margin-top: 0; margin-bottom: 20px; text-align: center; color: #333; font-size: 1.3em; }
      #googleSearchMultiSite-settings-dialog-content label { display: block; margin-bottom: 5px; color: #555; font-size: 0.95em; }
      #googleSearchMultiSite-settings-dialog-content input[type="text"], #googleSearchMultiSite-settings-dialog-content input[type="password"] {
        width: 100%; padding: 10px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; margin-bottom: 0; font-size: 1em;
      }
      #googleSearchMultiSite-settings-dialog-content small { font-size:0.8em; color:#777; display:block; margin-top:4px; margin-bottom:12px; }
      #googleSearchMultiSite-settings-dialog-content .rw-dialog-buttons { text-align: right; margin-top: 15px; }
      #googleSearchMultiSite-settings-dialog-content .rw-dialog-buttons button { padding: 10px 18px; border-radius: 4px; border: none; cursor: pointer; font-size: 0.95em; transition: background-color 0.2s ease; }
      #googleSearchMultiSite-settings-dialog-content .rw-dialog-buttons .rw-cancel-btn { margin-right: 10px; background-color: #f0f0f0; color: #333; }
      #googleSearchMultiSite-settings-dialog-content .rw-dialog-buttons .rw-cancel-btn:hover { background-color: #e0e0e0; }
      #googleSearchMultiSite-settings-dialog-content .rw-dialog-buttons .rw-save-btn { background-color: #4CAF50; color: white; }
      #googleSearchMultiSite-settings-dialog-content .rw-dialog-buttons .rw-save-btn:hover { background-color: #45a049; }
      #googleSearchMultiSite-settings-close-btn { position: absolute; top: 10px; right: 10px; font-size: 1.5em; color: #aaa; cursor: pointer; background: none; border: none; padding: 5px; line-height: 1; }
      #googleSearchMultiSite-settings-close-btn:hover { color: #777; }
    `)}}class G{static show(){const t=localStorage.getItem(s.STORAGE.LOCALSTORAGE_SITES_KEY)||s.DEFAULT_SITES,i=document.createElement("div");this.applyMaskStyles(i);const o=document.createElement("div");this.applyDialogStyles(o);const e=document.createElement("div");e.textContent="自定义站点配置",e.style.fontWeight="bold",e.style.fontSize="18px",e.style.marginBottom="10px",o.appendChild(e);const n=document.createElement("div");n.textContent='每一行代表一行按钮,每个按钮之间用空格分隔,每个按钮是"名字,网址"的格式:',n.style.fontSize="13px",n.style.color="#666",n.style.marginBottom="8px",o.appendChild(n);const l=document.createElement("textarea");l.value=t,this.setupTextarea(l),o.appendChild(l);const r=document.createElement("div");r.style.display="flex",r.style.justifyContent="flex-end",r.style.gap="12px";const d=this.createButton("保存","#4caf50",()=>{localStorage.setItem(s.STORAGE.LOCALSTORAGE_SITES_KEY,l.value),this.saveTextareaSize(l),document.body.removeChild(i),a("站点配置已保存,刷新页面后生效!","success")});r.appendChild(d);const u=this.createButton("取消","#eee",()=>{this.saveTextareaSize(l),document.body.removeChild(i)});r.appendChild(u),o.appendChild(r),i.appendChild(o),document.body.appendChild(i)}static applyMaskStyles(t){Object.assign(t.style,{position:"fixed",left:"0",top:"0",width:"100vw",height:"100vh",background:"rgba(0,0,0,0.25)",zIndex:s.UI.DIALOG.Z_INDEX,display:"flex",alignItems:"center",justifyContent:"center"})}static applyDialogStyles(t){Object.assign(t.style,{background:"#fff",borderRadius:"10px",boxShadow:"0 8px 32px rgba(0,0,0,0.18)",padding:"24px 24px 16px 24px",minWidth:"480px",maxWidth:"90vw",display:"flex",flexDirection:"column",alignItems:"stretch"})}static setupTextarea(t){const i=localStorage.getItem(s.STORAGE.LOCALSTORAGE_TEXTAREA_WIDTH_KEY),o=localStorage.getItem(s.STORAGE.LOCALSTORAGE_TEXTAREA_HEIGHT_KEY);Object.assign(t.style,{width:i||"100%",height:o||"240px",fontSize:"14px",padding:"10px",border:"1px solid #bbb",borderRadius:"6px",resize:"both",overflow:"auto",marginBottom:"16px",fontFamily:"monospace,Consolas,Menlo"})}static createButton(t,i,o){const e=document.createElement("button");return e.textContent=t,Object.assign(e.style,{padding:"6px 18px",background:i,color:i==="#eee"?"#333":"#fff",border:"none",borderRadius:"5px",fontSize:"15px",cursor:"pointer"}),e.onclick=o,e}static saveTextareaSize(t){localStorage.setItem(s.STORAGE.LOCALSTORAGE_TEXTAREA_WIDTH_KEY,t.style.width),localStorage.setItem(s.STORAGE.LOCALSTORAGE_TEXTAREA_HEIGHT_KEY,t.style.height)}}class m{static getGitHubToken(){return GM_getValue(s.STORAGE.GM_GITHUB_TOKEN_KEY,"")}static getGistId(){return GM_getValue(s.STORAGE.GM_GIST_ID_KEY,"")}static async makeRequest(t){const i=this.getGitHubToken();if(!i)throw new Error("GitHub Token 未配置");return t.headers={...t.headers,Authorization:`token ${i}`,Accept:"application/vnd.github.v3+json"},new Promise((o,e)=>{GM_xmlhttpRequest({...t,onload:n=>n.status>=200&&n.status<300?o(n):e(n),onerror:n=>e(n)})})}static async getGistFile(){const t=this.getGistId();if(!t)return null;try{const i=await this.makeRequest({method:"GET",url:`https://api.github.com/gists/${t}`}),o=JSON.parse(i.responseText),e=s.GIST.FILENAME;return o.files&&o.files[e]?(s.DEBUG_MODE,o.files[e]):(s.DEBUG_MODE,null)}catch(i){return i.status===404?a("Gist 未找到,请检查Gist ID配置","warning",5e3):a(`获取Gist文件失败: ${i.statusText||"Unknown error"}`,"error"),null}}static async updateGistFile(t){const i=this.getGistId();if(!i)return a("Gist ID 未配置","error"),!1;try{return await this.makeRequest({method:"PATCH",url:`https://api.github.com/gists/${i}`,headers:{"Content-Type":"application/json"},data:JSON.stringify({files:{[s.GIST.FILENAME]:{content:t}}})}),s.DEBUG_MODE,!0}catch(o){return a(`更新Gist文件失败: ${o.statusText||"Unknown error"}`,"error"),!1}}static async createGist(t){try{const i=await this.makeRequest({method:"POST",url:"https://api.github.com/gists",headers:{"Content-Type":"application/json"},data:JSON.stringify({description:s.GIST.DESCRIPTION,public:!1,files:{[s.GIST.FILENAME]:{content:t}}})}),o=JSON.parse(i.responseText);return s.DEBUG_MODE,o.id}catch(i){return a(`创建Gist失败: ${i.statusText||"Unknown error"}`,"error"),null}}static async uploadToGist(){if(!this.getGitHubToken()){a("GitHub Token 未配置。请通过油猴菜单「⚙️ 配置Gist同步参数」进行设置。","error");return}const i=localStorage.getItem(s.STORAGE.LOCALSTORAGE_SITES_KEY)||s.DEFAULT_SITES,o=this.getGistId(),e=a("上传配置到Gist中...","info",0);try{let n=!1,l=!1;if(o)n=await this.updateGistFile(i);else{const r=await this.createGist(i);r&&(GM_setValue(s.STORAGE.GM_GIST_ID_KEY,r),n=!0,l=!0)}e.remove(),n&&(l?a("新Gist已创建并自动保存!","success",7e3):a("配置已成功同步到Gist!","success"))}catch{e.remove()}}static async downloadFromGist(){if(!this.getGitHubToken()){a("GitHub Token 未配置。请通过油猴菜单「⚙️ 配置Gist同步参数」进行设置。","error");return}if(!this.getGistId()){a("Gist ID 未配置。请通过油猴菜单「⚙️ 配置Gist同步参数」进行设置,或先上传一次。","warning",5e3);return}const o=a("从Gist下载配置中...","info",0);try{const e=await this.getGistFile();if(o.remove(),e&&e.content){const n=localStorage.getItem(s.STORAGE.LOCALSTORAGE_SITES_KEY)||s.DEFAULT_SITES;if(e.content===n){a("本地配置与Gist中的一致,无需下载。","info");return}const l=e.content.split(`
`).length,r=n.split(`
`).length;confirm(`Gist含约 ${l} 行配置,本地含约 ${r} 行。
确定用Gist记录覆盖本地吗?(建议先上传备份本地配置)`)?(localStorage.setItem(s.STORAGE.LOCALSTORAGE_SITES_KEY,e.content),a("已从Gist下载并覆盖本地配置!将刷新页面应用。","success",3e3),setTimeout(()=>window.location.reload(),2e3)):a("已取消从Gist下载。","info")}else a("从Gist下载配置失败,未找到有效内容。","error")}catch{o.remove(),a("从Gist下载时发生错误。","error")}}}class x{static main(){console.log("[谷歌换搜] v1.0.0 开始运行"),document.readyState==="loading"?document.addEventListener("DOMContentLoaded",this.initialize.bind(this)):this.initialize()}static initialize(){this.registerMenuCommands(),setTimeout(()=>{h.init()},1e3)}static registerMenuCommands(){typeof GM_registerMenuCommand=="function"&&(GM_registerMenuCommand("⚙️ 配置Gist同步参数",()=>T.show()),GM_registerMenuCommand("⬆️ 上传配置到 Gist",()=>m.uploadToGist()),GM_registerMenuCommand("⬇️ 从 Gist 下载配置",()=>m.downloadFromGist()),GM_registerMenuCommand("⚙️ 设置站点",()=>G.show()))}}x.main()})();