谷歌搜图-增强

谷歌搜图增强功能,多站点搜索切换

// ==UserScript==
// @name         谷歌搜图-增强
// @version      1.1.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?*site%3A*
// @match        https://www.google.com/search?*site:*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// @license      MIT
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_openInTab
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      api.github.com
// @run-at       document-idle
// ==/UserScript==

(function(){"use strict";const CONFIG={DEBUG_MODE:!0,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:{searchBox:"textarea[name='q']",submitButton:"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"}}};class ModuleManager{static getPageType(){
const t=window.location.href,e=this.hasRealSiteParam(t)
;return e&&(t.includes("udm=2")||t.includes("tbm=isch"))?"site-image-search":e?"site-search":t.includes("udm=48")?"image-search":"normal-search"}
static hasRealSiteParam(t){if(t.includes("site%3A"))return!0;const e=new URLSearchParams(new URL(t).search).get("q")||""
;return/\bsite:[^\s]+\b/.test(e)}static shouldEnableMultiSiteFeatures(){const t=this.getPageType();return"site-search"===t||"site-image-search"===t}
static getPageTypeDescription(){switch(this.getPageType()){case"site-image-search":return"站点图片搜索页面";case"site-search":return"多站点搜索页面"
;case"image-search":return"以图搜图页面";case"normal-search":return"普通搜索页面";default:return"未知页面类型"}}static logPageType(){void 0}}function t(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 e(){
return(new URLSearchParams(window.location.search).get("q")||"").replace(/site:[^\s]+/g,"").trim()}function i(){
return document.querySelector(CONFIG.SELECTORS.searchBox)}class Storage{static get(t,e=null){try{const i=GM_getValue(t);if(null==i)return e;try{
return JSON.parse(i)}catch{return i}}catch(i){return void 0,e}}static set(t,e){try{const i=JSON.stringify(e);return GM_setValue(t,i),!0}catch(i){
return void 0,!1}}static delete(t){try{return GM_deleteValue(t),!0}catch(e){return void 0,!1}}static listKeys(){try{return GM_listValues()}catch(t){
return void 0,[]}}static migrateFromLocalStorage(t,e=!0){try{const i=localStorage.getItem(t);if(null!==i){try{const e=JSON.parse(i);this.set(t,e)
}catch{GM_setValue(t,i)}return e&&localStorage.removeItem(t),!0}return!1}catch(i){return void 0,!1}}}const s=class{static init(){
if(!i())return void 0,void 0;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=Storage.get(CONFIG.STORAGE.LOCALSTORAGE_NAVBAR_POSITION_KEY,null);if(t)try{const e=JSON.parse(t)
;this.navBar.style.top=e.top,this.navBar.style.left=e.left}catch(e){void 0}}static savePosition(){if(!this.navBar)return;const t={
top:this.navBar.style.top,left:this.navBar.style.left};Storage.set(CONFIG.STORAGE.LOCALSTORAGE_NAVBAR_POSITION_KEY,JSON.stringify(t))}
static createSiteButtons(){this.navBar&&t(Storage.get(CONFIG.STORAGE.LOCALSTORAGE_SITES_KEY,CONFIG.DEFAULT_SITES)||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 s=document.createElement("button")
;s.textContent=t,this.applyButtonStyles(s),s.addEventListener("click",()=>{this.performSearch(i)}),e.appendChild(s)}),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 s=i();if(!s)return;const n=e();s.value=`${n} site:${t}`
;const o=document.querySelector(CONFIG.SELECTORS.submitButton);o&&o.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")}))}static hideNavBar(){this.navBar&&(this.navBar.remove(),this.navBar=null)}static cleanup(){
this.hideNavBar()}};s.navBar=null,s.isDragging=!1,s.offsetX=0,s.offsetY=0;let n=s;class Toast{static show(t,e="info",i=3e3){
const s=document.createElement("div");s.id="toast-"+Date.now(),s.textContent=t,Object.assign(s.style,{position:"fixed",bottom:"20px",left:"50%",
transform:"translateX(-50%)",padding:"10px 20px",borderRadius:"5px",color:"white",zIndex:"99999",boxShadow:"0 2px 10px rgba(0,0,0,0.2)",opacity:"0",
transition:"opacity 0.5s ease-in-out",fontSize:"14px",whiteSpace:"nowrap",maxWidth:"80%",textAlign:"center"});const n={success:"#4CAF50",
error:"#F44336",warning:"#FF9800",info:"#2196F3"};return s.style.backgroundColor=n[e],document.body.appendChild(s),setTimeout(()=>{s.style.opacity="1"
},10),i>0&&setTimeout(()=>{s.style.opacity="0",setTimeout(()=>{s.remove()},500)},i),s}static success(t,e=3e3){return this.show(t,"success",e)}
static error(t,e=3e3){return this.show(t,"error",e)}static warning(t,e=3e3){return this.show(t,"warning",e)}static info(t,e=3e3){
return this.show(t,"info",e)}}function o(t,e){{const t=document.getElementById(e);if(t)return t}const i=document.createElement("style");return i.id=e,
i.textContent=t,document.head.appendChild(i),i}class SettingsPanel{static show(){void 0
;const t=document.getElementById("googleSearchMultiSite-settings-dialog");t&&t.remove();const e=document.createElement("div")
;e.id="googleSearchMultiSite-settings-dialog";const i=document.createElement("div");i.id="googleSearchMultiSite-settings-dialog-content",
i.innerHTML=`\n      <button id="googleSearchMultiSite-settings-close-btn" title="关闭">&times;</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="${Storage.get(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="${Storage.get(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    `,
e.appendChild(i),document.body.appendChild(e),this.applyStyles();const s=t=>{"Escape"===t.key&&n()},n=()=>{document.removeEventListener("keydown",s),
e.remove()},o=()=>{const t=document.getElementById("gist_token_input_gsms").value.trim(),e=document.getElementById("gist_id_input_gsms").value.trim()
;Storage.set(CONFIG.STORAGE.GM_GITHUB_TOKEN_KEY,t),Storage.set(CONFIG.STORAGE.GM_GIST_ID_KEY,e);let i="Gist参数已保存!"
;t||e?t?e||(i="Gist ID已清空, Token已保存。"):i="Token已清空, Gist ID已保存。":i="Gist参数已清空。",Toast.show(i,"success"),n()},a=()=>{n(),Toast.show("参数设置已取消。","info")}
;document.getElementById("settings_save_btn_gsms")?.addEventListener("click",o),
document.getElementById("settings_cancel_btn_gsms")?.addEventListener("click",a),
document.getElementById("googleSearchMultiSite-settings-close-btn")?.addEventListener("click",a),document.addEventListener("keydown",s)}
static applyStyles(){
o(`\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    `,"googleSearchMultiSite-settings-styles")
}}class SiteConfigEditor{static show(){
const t=Storage.get(CONFIG.STORAGE.LOCALSTORAGE_SITES_KEY,CONFIG.DEFAULT_SITES)||CONFIG.DEFAULT_SITES,e=document.createElement("div")
;this.applyMaskStyles(e);const i=document.createElement("div");this.applyDialogStyles(i);const s=document.createElement("div")
;s.textContent="自定义站点配置",s.style.fontWeight="bold",s.style.fontSize="18px",s.style.marginBottom="10px",i.appendChild(s)
;const n=document.createElement("div");n.textContent='每一行代表一行按钮,每个按钮之间用空格分隔,每个按钮是"名字,网址"的格式:',n.style.fontSize="13px",n.style.color="#666",
n.style.marginBottom="8px",i.appendChild(n);const o=document.createElement("textarea");o.value=t,this.setupTextarea(o),i.appendChild(o)
;const a=document.createElement("div");a.style.display="flex",a.style.justifyContent="flex-end",a.style.gap="12px"
;const r=this.createButton("保存","#4caf50",()=>{Storage.set(CONFIG.STORAGE.LOCALSTORAGE_SITES_KEY,o.value),this.saveTextareaSize(o),
document.body.removeChild(e),Toast.show("站点配置已保存,刷新页面后生效!","success")});a.appendChild(r);const c=this.createButton("取消","#eee",()=>{
this.saveTextareaSize(o),document.body.removeChild(e)});a.appendChild(c),i.appendChild(a),e.appendChild(i),document.body.appendChild(e)}
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=Storage.get(CONFIG.STORAGE.LOCALSTORAGE_TEXTAREA_WIDTH_KEY,null),i=Storage.get(CONFIG.STORAGE.LOCALSTORAGE_TEXTAREA_HEIGHT_KEY,null)
;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 s=document.createElement("button")
;return s.textContent=t,Object.assign(s.style,{padding:"6px 18px",background:e,color:"#eee"===e?"#333":"#fff",border:"none",borderRadius:"5px",
fontSize:"15px",cursor:"pointer"}),s.onclick=i,s}static saveTextareaSize(t){Storage.set(CONFIG.STORAGE.LOCALSTORAGE_TEXTAREA_WIDTH_KEY,t.style.width),
Storage.set(CONFIG.STORAGE.LOCALSTORAGE_TEXTAREA_HEIGHT_KEY,t.style.height)}}class GistAPI{static async request(t,config){
if(!t)throw new Error("GitHub Token 未提供");const e={...config,headers:{...config.headers,Authorization:`token ${t}`,
Accept:"application/vnd.github.v3+json"}};return new Promise((t,i)=>{GM_xmlhttpRequest({...e,onload:e=>{e.status>=200&&e.status<300?t(e):i(e)},
onerror:t=>i(t)})})}static async getFile(t,e,i){if(!e)throw new Error("Gist ID 未提供");try{const s=await this.request(t,{method:"GET",
url:`https://api.github.com/gists/${e}`}),n=JSON.parse(s.responseText);return n.files&&n.files[i]?n.files[i]:null}catch(s){throw s}}
static async updateFile(t,e,i,s){if(!e)throw new Error("Gist ID 未提供");try{return await this.request(t,{method:"PATCH",
url:`https://api.github.com/gists/${e}`,headers:{"Content-Type":"application/json"},data:JSON.stringify({files:{[i]:{content:s}}})}),!0}catch(n){
throw n}}static async createGist(t,e,i,s,n=!1){try{const o=await this.request(t,{method:"POST",url:"https://api.github.com/gists",headers:{
"Content-Type":"application/json"},data:JSON.stringify({description:s,public:n,files:{[e]:{content:i}}})});return JSON.parse(o.responseText).id
}catch(o){throw o}}static async deleteGist(t,e){if(!e)throw new Error("Gist ID 未提供");try{return await this.request(t,{method:"DELETE",
url:`https://api.github.com/gists/${e}`}),!0}catch(i){throw i}}}class GistSync{static getGitHubToken(){
return Storage.get(CONFIG.STORAGE.GM_GITHUB_TOKEN_KEY,"")||""}static getGistId(){return Storage.get(CONFIG.STORAGE.GM_GIST_ID_KEY,"")||""}
static async getGistFile(){const t=this.getGitHubToken(),e=this.getGistId();if(!t)return void 0,null;if(!e)return void 0,null;try{
const i=await GistAPI.getFile(t,e,CONFIG.GIST.FILENAME);return i?(CONFIG.DEBUG_MODE,0,i):(CONFIG.DEBUG_MODE,0,null)}catch(i){
return 404===i.status?Toast.show("Gist 未找到,请检查Gist ID配置","warning",5e3):Toast.show(`获取Gist文件失败: ${i.statusText||"Unknown error"}`,"error"),null}}
static async updateGistFile(t){const e=this.getGitHubToken(),i=this.getGistId();if(!e||!i)return Toast.show("GitHub Token 或 Gist ID 未配置","error"),!1
;try{return await GistAPI.updateFile(e,i,CONFIG.GIST.FILENAME,t),CONFIG.DEBUG_MODE,!0}catch(s){
return Toast.show(`更新Gist文件失败: ${s.statusText||"Unknown error"}`,"error"),!1}}static async createGist(t){const e=this.getGitHubToken()
;if(!e)return Toast.show("GitHub Token 未配置","error"),null;try{const i=await GistAPI.createGist(e,CONFIG.GIST.FILENAME,t,CONFIG.GIST.DESCRIPTION,!1)
;return CONFIG.DEBUG_MODE,0,i}catch(i){return Toast.show(`创建Gist失败: ${i.statusText||"Unknown error"}`,"error"),null}}static async uploadToGist(){
if(!this.getGitHubToken())return Toast.show("GitHub Token 未配置。请通过油猴菜单「⚙️ 配置Gist同步参数」进行设置。","error"),void 0
;const t=localStorage.getItem(CONFIG.STORAGE.LOCALSTORAGE_SITES_KEY)||CONFIG.DEFAULT_SITES,e=this.getGistId(),i=Toast.show("上传配置到Gist中...","info",0)
;try{let s=!1,n=!1;if(e)s=await this.updateGistFile(t);else{const e=await this.createGist(t);e&&(Storage.set(CONFIG.STORAGE.GM_GIST_ID_KEY,e),s=!0,
n=!0)}i.remove(),s&&(n?Toast.show("新Gist已创建并自动保存!","success",7e3):Toast.show("配置已成功同步到Gist!","success"))}catch(s){i.remove()}}
static async downloadFromGist(){if(!this.getGitHubToken())return Toast.show("GitHub Token 未配置。请通过油猴菜单「⚙️ 配置Gist同步参数」进行设置。","error"),void 0
;if(!this.getGistId())return Toast.show("Gist ID 未配置。请通过油猴菜单「⚙️ 配置Gist同步参数」进行设置,或先上传一次。","warning",5e3),void 0
;const t=Toast.show("从Gist下载配置中...","info",0);try{const e=await this.getGistFile();if(t.remove(),e&&e.content){
const t=localStorage.getItem(CONFIG.STORAGE.LOCALSTORAGE_SITES_KEY)||CONFIG.DEFAULT_SITES
;if(e.content===t)return Toast.show("本地配置与Gist中的一致,无需下载。","info"),void 0;const i=e.content.split("\n").length,s=t.split("\n").length
;confirm(`Gist含约 ${i} 行配置,本地含约 ${s} 行。\n确定用Gist记录覆盖本地吗?(建议先上传备份本地配置)`)?(localStorage.setItem(CONFIG.STORAGE.LOCALSTORAGE_SITES_KEY,e.content),
Toast.show("已从Gist下载并覆盖本地配置!将刷新页面应用。","success",3e3),setTimeout(()=>window.location.reload(),2e3)):Toast.show("已取消从Gist下载。","info")
}else Toast.show("从Gist下载配置失败,未找到有效内容。","error")}catch(e){t.remove(),Toast.show("从Gist下载时发生错误。","error")}}}class GoogleMultiSiteSearchApp{
static main(){void 0,"loading"===document.readyState?document.addEventListener("DOMContentLoaded",this.initialize.bind(this)):this.initialize()}
static initialize(){try{const t=ModuleManager.getPageType();if("image-search"===t||"normal-search"===t)return CONFIG.DEBUG_MODE,0,void 0
;this.registerMenuCommands(),this.initializeModules(),setTimeout(()=>{ModuleManager.shouldEnableMultiSiteFeatures()&&n.init()},1e3),CONFIG.DEBUG_MODE
}catch(t){void 0}}static registerMenuCommands(){
ModuleManager.shouldEnableMultiSiteFeatures()&&"function"==typeof GM_registerMenuCommand?(GM_registerMenuCommand("⚙️ 配置Gist同步参数",()=>SettingsPanel.show()),
GM_registerMenuCommand("⬆️ 上传配置到 Gist",()=>GistSync.uploadToGist()),GM_registerMenuCommand("⬇️ 从 Gist 下载配置",()=>GistSync.downloadFromGist()),
GM_registerMenuCommand("⚙️ 设置站点",()=>SiteConfigEditor.show())):void 0}static initializeModules(){ModuleManager.logPageType(),
ModuleManager.shouldEnableMultiSiteFeatures(),"normal-search"===ModuleManager.getPageType()}static cleanup(){try{n.cleanup()}catch(t){void 0}}}
GoogleMultiSiteSearchApp.main()})();