// ==UserScript==
// @name 谷歌换搜
// @version 1.0.1
// @namespace https://sleazyfork.org/zh-CN/users/1461640-%E6%98%9F%E5%AE%BF%E8%80%81%E9%AD%94
// @author 星宿老魔
// @description 谷歌搜图-多站点搜索切换功能
// @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
// @connect api.github.com
// @run-at document-end
// ==/UserScript==
(function(){"use strict";const CONFIG={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("\n"),
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 t(t,e="info",i=3e3){
const n="userscript-notification-"+Date.now(),o=document.createElement("div");switch(o.id=n,o.textContent=t,Object.assign(o.style,{position:"fixed",
bottom:"20px",left:"50%",transform:"translateX(-50%)",padding:"10px 20px",borderRadius:"5px",color:"white",zIndex:CONFIG.UI.NOTIFICATION.Z_INDEX,
boxShadow:"0 2px 10px rgba(0,0,0,0.2)",opacity:"0",transition:"opacity 0.5s ease-in-out"}),e){case"success":o.style.backgroundColor="#4CAF50";break
;case"error":o.style.backgroundColor="#F44336";break;case"warning":o.style.backgroundColor="#FF9800";break;default:o.style.backgroundColor="#2196F3"}
return document.body.appendChild(o),setTimeout(()=>{o.style.opacity="1"},10),i>0&&setTimeout(()=>{o.style.opacity="0",setTimeout(()=>{o.remove()},500)
},i),o}function e(t){return t.split("\n").map(t=>t.trim().split(/\s+/).filter(Boolean).map(t=>{const[e,i]=t.split(",");return{name:e||"",url:i||""}
}).filter(t=>t.name&&t.url)).filter(t=>t.length>0)}function i(){
return(new URLSearchParams(window.location.search).get("q")||"").replace(/site:[^\s]+/g,"").trim()}function n(){
return document.querySelector(CONFIG.SELECTORS.SEARCH_BOX)}const o=class{static init(){n()&&(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:CONFIG.UI.NAVBAR.DEFAULT_POSITION.top,left:CONFIG.UI.NAVBAR.DEFAULT_POSITION.left,zIndex:CONFIG.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(CONFIG.STORAGE.LOCALSTORAGE_NAVBAR_POSITION_KEY);if(t)try{const e=JSON.parse(t);this.navBar.style.top=e.top,
this.navBar.style.left=e.left}catch(e){}}static savePosition(){if(!this.navBar)return;const t={top:this.navBar.style.top,left:this.navBar.style.left}
;localStorage.setItem(CONFIG.STORAGE.LOCALSTORAGE_NAVBAR_POSITION_KEY,JSON.stringify(t))}static createSiteButtons(){
this.navBar&&e(localStorage.getItem(CONFIG.STORAGE.LOCALSTORAGE_SITES_KEY)||CONFIG.DEFAULT_SITES).forEach(t=>{const e=document.createElement("div")
;e.style.display="flex",e.style.gap="5px",t.forEach(({name:t,url:i})=>{const n=document.createElement("button");n.textContent=t,
this.applyButtonStyles(n),n.addEventListener("click",()=>{this.performSearch(i)}),e.appendChild(n)}),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 e=n();if(!e)return;const o=i();e.value=`${o} site:${t}`
;const s=document.querySelector(CONFIG.SELECTORS.SUBMIT_BUTTON);s&&s.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")}))}};o.navBar=null,o.isDragging=!1,o.offsetX=0,o.offsetY=0;let s=o;class SettingsPanel{static show(){
const e=document.getElementById("googleSearchMultiSite-settings-dialog");e&&e.remove();const i=document.createElement("div")
;i.id="googleSearchMultiSite-settings-dialog";const n=document.createElement("div");n.id="googleSearchMultiSite-settings-dialog-content",
n.innerHTML=`\n <button id="googleSearchMultiSite-settings-close-btn" title="关闭">×</button>\n <h3>Gist 同步参数配置</h3>\n <div>\n <label for="gist_token_input_gsms">GitHub 个人访问令牌 (Token):</label>\n <input type="password" id="gist_token_input_gsms" value="${GM_getValue(CONFIG.STORAGE.GM_GITHUB_TOKEN_KEY,"")}" placeholder="例如 ghp_xxxxxxxxxxxxxxxxx">\n <small>Token 用于授权访问您的Gist。需要 Gist 读写权限。</small>\n </div>\n <div>\n <label for="gist_id_input_gsms">Gist ID:</label>\n <input type="text" id="gist_id_input_gsms" value="${GM_getValue(CONFIG.STORAGE.GM_GIST_ID_KEY,"")}" placeholder="例如 123abc456def7890">\n <small>Gist ID 是备份用Gist的标识。若为空,首次上传时将自动创建并保存。</small>\n </div>\n <div class="rw-dialog-buttons">\n <button id="settings_cancel_btn_gsms" class="rw-cancel-btn">取消</button>\n <button id="settings_save_btn_gsms" class="rw-save-btn">保存配置</button>\n </div>\n `,
i.appendChild(n),document.body.appendChild(i),this.applyStyles();const o=t=>{"Escape"===t.key&&s()},s=()=>{document.removeEventListener("keydown",o),
i.remove()},a=()=>{const e=document.getElementById("gist_token_input_gsms").value.trim(),i=document.getElementById("gist_id_input_gsms").value.trim()
;GM_setValue(CONFIG.STORAGE.GM_GITHUB_TOKEN_KEY,e),GM_setValue(CONFIG.STORAGE.GM_GIST_ID_KEY,i);let n="Gist参数已保存!"
;e||i?e?i||(n="Gist ID已清空, Token已保存。"):n="Token已清空, Gist ID已保存。":n="Gist参数已清空。",t(n,"success"),s()},r=()=>{s(),t("参数设置已取消。","info")}
;document.getElementById("settings_save_btn_gsms")?.addEventListener("click",a),
document.getElementById("settings_cancel_btn_gsms")?.addEventListener("click",r),
document.getElementById("googleSearchMultiSite-settings-close-btn")?.addEventListener("click",r),document.addEventListener("keydown",o)}
static applyStyles(){
GM_addStyle(`\n #googleSearchMultiSite-settings-dialog {\n position: fixed; top: 0; left: 0; width: 100%; height: 100%;\n background-color: rgba(0,0,0,0.6); z-index: ${CONFIG.UI.DIALOG.SETTINGS_Z_INDEX};\n display: flex; justify-content: center; align-items: center; font-family: sans-serif;\n }\n #googleSearchMultiSite-settings-dialog-content {\n background: white; padding: 25px; border-radius: 8px;\n box-shadow: 0 5px 20px rgba(0,0,0,0.3); width: 400px; max-width: 90%;\n position: relative;\n }\n #googleSearchMultiSite-settings-dialog-content h3 { margin-top: 0; margin-bottom: 20px; text-align: center; color: #333; font-size: 1.3em; }\n #googleSearchMultiSite-settings-dialog-content label { display: block; margin-bottom: 5px; color: #555; font-size: 0.95em; }\n #googleSearchMultiSite-settings-dialog-content input[type="text"], #googleSearchMultiSite-settings-dialog-content input[type="password"] {\n width: 100%; padding: 10px; box-sizing: border-box; border: 1px solid #ccc; border-radius: 4px; margin-bottom: 0; font-size: 1em;\n }\n #googleSearchMultiSite-settings-dialog-content small { font-size:0.8em; color:#777; display:block; margin-top:4px; margin-bottom:12px; }\n #googleSearchMultiSite-settings-dialog-content .rw-dialog-buttons { text-align: right; margin-top: 15px; }\n #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; }\n #googleSearchMultiSite-settings-dialog-content .rw-dialog-buttons .rw-cancel-btn { margin-right: 10px; background-color: #f0f0f0; color: #333; }\n #googleSearchMultiSite-settings-dialog-content .rw-dialog-buttons .rw-cancel-btn:hover { background-color: #e0e0e0; }\n #googleSearchMultiSite-settings-dialog-content .rw-dialog-buttons .rw-save-btn { background-color: #4CAF50; color: white; }\n #googleSearchMultiSite-settings-dialog-content .rw-dialog-buttons .rw-save-btn:hover { background-color: #45a049; }\n #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; }\n #googleSearchMultiSite-settings-close-btn:hover { color: #777; }\n `)
}}class SiteConfigEditor{static show(){
const e=localStorage.getItem(CONFIG.STORAGE.LOCALSTORAGE_SITES_KEY)||CONFIG.DEFAULT_SITES,i=document.createElement("div");this.applyMaskStyles(i)
;const n=document.createElement("div");this.applyDialogStyles(n);const o=document.createElement("div");o.textContent="自定义站点配置",
o.style.fontWeight="bold",o.style.fontSize="18px",o.style.marginBottom="10px",n.appendChild(o);const s=document.createElement("div")
;s.textContent='每一行代表一行按钮,每个按钮之间用空格分隔,每个按钮是"名字,网址"的格式:',s.style.fontSize="13px",s.style.color="#666",s.style.marginBottom="8px",n.appendChild(s)
;const a=document.createElement("textarea");a.value=e,this.setupTextarea(a),n.appendChild(a);const r=document.createElement("div")
;r.style.display="flex",r.style.justifyContent="flex-end",r.style.gap="12px";const l=this.createButton("保存","#4caf50",()=>{
localStorage.setItem(CONFIG.STORAGE.LOCALSTORAGE_SITES_KEY,a.value),this.saveTextareaSize(a),document.body.removeChild(i),
t("站点配置已保存,刷新页面后生效!","success")});r.appendChild(l);const c=this.createButton("取消","#eee",()=>{this.saveTextareaSize(a),document.body.removeChild(i)})
;r.appendChild(c),n.appendChild(r),i.appendChild(n),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:CONFIG.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 e=localStorage.getItem(CONFIG.STORAGE.LOCALSTORAGE_TEXTAREA_WIDTH_KEY),i=localStorage.getItem(CONFIG.STORAGE.LOCALSTORAGE_TEXTAREA_HEIGHT_KEY)
;Object.assign(t.style,{width:e||"100%",height:i||"240px",fontSize:"14px",padding:"10px",border:"1px solid #bbb",borderRadius:"6px",resize:"both",
overflow:"auto",marginBottom:"16px",fontFamily:"monospace,Consolas,Menlo"})}static createButton(t,e,i){const n=document.createElement("button")
;return n.textContent=t,Object.assign(n.style,{padding:"6px 18px",background:e,color:"#eee"===e?"#333":"#fff",border:"none",borderRadius:"5px",
fontSize:"15px",cursor:"pointer"}),n.onclick=i,n}static saveTextareaSize(t){
localStorage.setItem(CONFIG.STORAGE.LOCALSTORAGE_TEXTAREA_WIDTH_KEY,t.style.width),
localStorage.setItem(CONFIG.STORAGE.LOCALSTORAGE_TEXTAREA_HEIGHT_KEY,t.style.height)}}class GistSync{static getGitHubToken(){
return GM_getValue(CONFIG.STORAGE.GM_GITHUB_TOKEN_KEY,"")}static getGistId(){return GM_getValue(CONFIG.STORAGE.GM_GIST_ID_KEY,"")}
static async makeRequest(t){const e=this.getGitHubToken();if(!e)throw new Error("GitHub Token 未配置");return t.headers={...t.headers,
Authorization:`token ${e}`,Accept:"application/vnd.github.v3+json"},new Promise((e,i)=>{GM_xmlhttpRequest({...t,
onload:t=>t.status>=200&&t.status<300?e(t):i(t),onerror:t=>i(t)})})}static async getGistFile(){const e=this.getGistId();if(!e)return null;try{
const t=await this.makeRequest({method:"GET",url:`https://api.github.com/gists/${e}`}),i=JSON.parse(t.responseText),n=CONFIG.GIST.FILENAME
;return i.files&&i.files[n]?(CONFIG.DEBUG_MODE,i.files[n]):(CONFIG.DEBUG_MODE,null)}catch(i){
return 404===i.status?t("Gist 未找到,请检查Gist ID配置","warning",5e3):t(`获取Gist文件失败: ${i.statusText||"Unknown error"}`,"error"),null}}
static async updateGistFile(e){const i=this.getGistId();if(!i)return t("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:{[CONFIG.GIST.FILENAME]:{content:e}}})
}),CONFIG.DEBUG_MODE,!0}catch(n){return t(`更新Gist文件失败: ${n.statusText||"Unknown error"}`,"error"),!1}}static async createGist(e){try{
const t=await this.makeRequest({method:"POST",url:"https://api.github.com/gists",headers:{"Content-Type":"application/json"},data:JSON.stringify({
description:CONFIG.GIST.DESCRIPTION,public:!1,files:{[CONFIG.GIST.FILENAME]:{content:e}}})}),i=JSON.parse(t.responseText);return CONFIG.DEBUG_MODE,
i.id}catch(i){return t(`创建Gist失败: ${i.statusText||"Unknown error"}`,"error"),null}}static async uploadToGist(){
if(!this.getGitHubToken())return t("GitHub Token 未配置。请通过油猴菜单「⚙️ 配置Gist同步参数」进行设置。","error"),void 0
;const e=localStorage.getItem(CONFIG.STORAGE.LOCALSTORAGE_SITES_KEY)||CONFIG.DEFAULT_SITES,i=this.getGistId(),n=t("上传配置到Gist中...","info",0);try{
let o=!1,s=!1;if(i)o=await this.updateGistFile(e);else{const t=await this.createGist(e);t&&(GM_setValue(CONFIG.STORAGE.GM_GIST_ID_KEY,t),o=!0,s=!0)}
n.remove(),o&&(s?t("新Gist已创建并自动保存!","success",7e3):t("配置已成功同步到Gist!","success"))}catch(o){n.remove()}}static async downloadFromGist(){
if(!this.getGitHubToken())return t("GitHub Token 未配置。请通过油猴菜单「⚙️ 配置Gist同步参数」进行设置。","error"),void 0
;if(!this.getGistId())return t("Gist ID 未配置。请通过油猴菜单「⚙️ 配置Gist同步参数」进行设置,或先上传一次。","warning",5e3),void 0;const e=t("从Gist下载配置中...","info",0);try{
const i=await this.getGistFile();if(e.remove(),i&&i.content){const e=localStorage.getItem(CONFIG.STORAGE.LOCALSTORAGE_SITES_KEY)||CONFIG.DEFAULT_SITES
;if(i.content===e)return t("本地配置与Gist中的一致,无需下载。","info"),void 0;const n=i.content.split("\n").length,o=e.split("\n").length
;confirm(`Gist含约 ${n} 行配置,本地含约 ${o} 行。\n确定用Gist记录覆盖本地吗?(建议先上传备份本地配置)`)?(localStorage.setItem(CONFIG.STORAGE.LOCALSTORAGE_SITES_KEY,i.content),
t("已从Gist下载并覆盖本地配置!将刷新页面应用。","success",3e3),setTimeout(()=>window.location.reload(),2e3)):t("已取消从Gist下载。","info")
}else t("从Gist下载配置失败,未找到有效内容。","error")}catch(i){e.remove(),t("从Gist下载时发生错误。","error")}}}class GoogleSearchMultiSite{static main(){void 0,
"loading"===document.readyState?document.addEventListener("DOMContentLoaded",this.initialize.bind(this)):this.initialize()}static initialize(){
this.registerMenuCommands(),setTimeout(()=>{s.init()},1e3)}static registerMenuCommands(){
"function"==typeof GM_registerMenuCommand&&(GM_registerMenuCommand("⚙️ 配置Gist同步参数",()=>SettingsPanel.show()),
GM_registerMenuCommand("⬆️ 上传配置到 Gist",()=>GistSync.uploadToGist()),GM_registerMenuCommand("⬇️ 从 Gist 下载配置",()=>GistSync.downloadFromGist()),
GM_registerMenuCommand("⚙️ 设置站点",()=>SiteConfigEditor.show()))}}GoogleSearchMultiSite.main()})();