// ==UserScript== // @name 以图搜图增强版 // @name:en Enhanced Reverse Image Search // @namespace https://github.com/belingud/GM.search-by-image // @version 0.9.0 // @description 以图搜图增强版,可以使用本地文件、粘贴链接、点击网页图片方式来搜图。支持谷歌Lens、TinEye、Yandex、Bing、搜狗、百度、trace、SauceNAO、IQDB、3DIQDB、ascii2d搜索引擎。 // @description:en Enhanced Reverse image search. You can search images using local files, pasting links, and clicking web images. Supports Google Lens, TinEye, Yandex, Bing, Sogou, Baidu, trace, SauceNAO, IQDB, 3DIQDB, ascii2d search engines. // @author belingud // @license BSD 3-Clause License // @match *://*/* // @grant GM_openInTab // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @connect 0x0.st // ==/UserScript== (function () { "use strict"; // Define current language const currentLanguage = navigator.language.includes("zh") ? "zh" : "en"; // Define translations const translations = { en: { selectImageSource: "Select Image Source:", selectSearchEngine: "Select Search Engine:", selectFile: "Select File", pasteURL: "Paste URL", clickImage: "Click Image", googleLens: "Google Lens", tinEye: "TinEye", yandex: "Yandex", bing: "Bing", sogou: "Sogou", baidu: "Baidu", trace: "trace", sauceNAO: "SauceNAO", IQDB: "IQDB", "3DIQDB": "3DIQDB", ascii2d: "ascii2d", close: "Close", loading: "Uploading...", pasteURLPrompt: "Paste image URL:", urlPasted: "URL pasted. Now choose a search engine.", clickAnyImage: "Click on any image on the page.", imageSelected: "Image selected. Now choose a search engine.", selectImageFirst: "Please select an image source first.", pleaseSelectFile: "Please select a file before clicking a search engine.", uploadError: "Upload failed, please try again or check your network.", dragHint: "Click and drag to move" }, zh: { selectImageSource: "选择图片来源:", selectSearchEngine: "选择搜索引擎:", selectFile: "选择文件", pasteURL: "粘贴链接", clickImage: "点击图片", googleLens: "Google Lens", tinEye: "TinEye", yandex: "Yandex", bing: "必应", sogou: "搜狗", baidu: "百度", trace: "trace", sauceNAO: "SauceNAO", IQDB: "IQDB", "3DIQDB": "3DIQDB", ascii2d: "ascii2d", close: "关闭", loading: "图片上传中...", pasteURLPrompt: "粘贴图片链接:", urlPasted: "链接已粘贴。现在选择一个搜索引擎。", clickAnyImage: "点击页面上的任何图片。", imageSelected: "图片已选择。现在选择一个搜索引擎。", selectImageFirst: "请先选择图片来源。", pleaseSelectFile: "请先选择文件,然后再点击搜索引擎。", uploadError: "上传失败,请重试或检查网络。", dragHint: "点击空白处拖动" }, }; // Helper function for translations function lang(key) { return translations[currentLanguage][key]; } let imageSrc = ""; // Image source URL let selectedEngine = ""; // Selected search engine let imgType = ""; // Image type let file = ""; // File object const searchUrl = { "Google Lens": "https://lens.google.com/uploadbyurl?url=${url}", TinEye: "https://www.tineye.com/search/?url=${url}", Yandex: "https://yandex.com/images/search?url=${url}&rpt=imageview", Bing: "https://www.bing.com/images/search?q=imgurl:${url}&view=detailv2&iss=sbi", Sogou: "https://pic.sogou.com/ris?query=https%3A%2F%2Fimg03.sogoucdn.com%2Fv2%2Fthumb%2Fretype_exclude_gif%2Fext%2Fauto%3Fappid%3D122%26url%3D${url}&flag=1&drag=0", Baidu: "https://graph.baidu.com/details?isfromtusoupc=1&tn=pc&carousel=0&promotion_name=pc_image_shituindex&extUiData%5bisLogoShow%5d=1&image=${url}", Trace: "https://trace.moe/?url=${url}", SauceNAO: "https://saucenao.com/search.php?db=999&url=${url}", IQDB: "https://iqdb.org/?url=${url}", "3DIQDB": "https://3d.iqdb.org/?url=${url}", ascii2d: "https://ascii2d.net/search/url/${url}", }; const searchEngines = [ { text: lang("googleLens"), handler: async () => { selectedEngine = "Google Lens"; await searchImage(); }, }, { text: lang("tinEye"), handler: async () => { selectedEngine = "TinEye"; await searchImage(); }, }, { text: lang("yandex"), handler: async () => { selectedEngine = "Yandex"; await searchImage(); }, }, { text: lang("bing"), handler: async () => { selectedEngine = "Bing"; await searchImage(); }, }, { text: lang("sogou"), handler: async () => { selectedEngine = "Sogou"; await searchImage(); }, }, { text: lang("baidu"), handler: async () => { selectedEngine = "Baidu"; await searchImage(); }, }, { text: lang("trace"), handler: async () => { selectedEngine = "Trace"; await searchImage(); }, }, { text: lang("sauceNAO"), handler: async () => { selectedEngine = "SauceNAO"; await searchImage(); }, }, { text: lang("IQDB"), handler: async () => { selectedEngine = "IQDB"; await searchImage(); }, }, { text: lang("3DIQDB"), handler: async () => { selectedEngine = "3DIQDB"; await searchImage(); }, }, { text: lang("ascii2d"), handler: async () => { selectedEngine = "ascii2d"; await searchImage(); }, }, ]; const imageSources = [ { text: lang("selectFile"), handler: selectFile, id: "select-file" }, { text: lang("pasteURL"), handler: pasteURL, id: "paste-url" }, { text: lang("clickImage"), handler: clickImage, id: "click-image" }, ]; // Register the main command in the Tampermonkey menu GM_registerMenuCommand("Reverse Image Search", openMenu); // Function to create and open the menu function openMenu() { // Remove any existing menu const existingMenu = document.getElementById("reverse-image-search-menu"); if (existingMenu) { existingMenu.remove(); } const menu = document.createElement("div"); menu.id = "reverse-image-search-menu"; menu.style.position = "fixed"; menu.style.top = "10px"; menu.style.right = "10px"; menu.style.backgroundColor = "#fff"; menu.style.border = "1px solid #ccc"; menu.style.zIndex = "9999"; menu.style.padding = "10px"; menu.style.maxWidth = "200px"; // font color black menu.style.color = "black"; document.body.appendChild(menu); // Make the menu draggable makeDraggable(menu); // Image source options const sourceTitle = document.createElement("div"); sourceTitle.textContent = lang("selectImageSource"); sourceTitle.style.marginBottom = "10px"; sourceTitle.style.fontWeight = "bold"; menu.appendChild(sourceTitle); imageSources.forEach((source) => { const sourceOption = document.createElement("div"); sourceOption.textContent = source.text; sourceOption.id = source.id; sourceOption.style.cursor = "pointer"; sourceOption.style.padding = "5px"; sourceOption.style.textAlign = "center"; sourceOption.style.border = "1px solid #ddd"; sourceOption.style.marginBottom = "5px"; sourceOption.addEventListener("click", source.handler); menu.appendChild(sourceOption); }); // Search engine buttons const engineTitle = document.createElement("div"); engineTitle.textContent = lang("selectSearchEngine"); engineTitle.style.marginBottom = "10px"; engineTitle.style.fontWeight = "bold"; menu.appendChild(engineTitle); searchEngines.forEach((engine) => { const engineOption = document.createElement("div"); engineOption.textContent = engine.text; engineOption.style.cursor = "pointer"; engineOption.style.padding = "5px"; engineOption.style.textAlign = "center"; engineOption.style.border = "1px solid #ddd"; engineOption.style.marginBottom = "5px"; engineOption.addEventListener("click", async () => { if (imgType === "file" && file) { showLoading(menu); // Show loading animation } await engine.handler(); }); menu.appendChild(engineOption); }); // Add drag hint const dragHint = document.createElement("div"); dragHint.textContent = lang("dragHint"); dragHint.style.fontStyle = "italic"; dragHint.style.fontSize = "10px"; dragHint.style.marginTop = "10px"; menu.appendChild(dragHint); const closeButton = document.createElement("button"); closeButton.textContent = lang("close"); closeButton.style.marginTop = "10px"; closeButton.style.padding = "5px"; closeButton.style.width = "100%"; closeButton.addEventListener("click", () => { menu.remove(); }); menu.appendChild(closeButton); // Add spinner animation keyframes const style = document.createElement("style"); style.type = "text/css"; style.innerHTML = ` @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } `; document.head.appendChild(style); } // Handle select file function selectFile() { imgType = "file"; const input = document.createElement("input"); input.type = "file"; input.accept = "image/*"; input.addEventListener("change", (event) => { file = event.target.files[0]; markSelected("select-file"); }); input.click(); } // Handle paste URL function pasteURL() { imgType = "url"; const url = prompt(lang("pasteURLPrompt")); if (url) { imageSrc = url; markSelected("paste-url"); } } // Handle click image function clickImage() { imgType = "page"; document.body.addEventListener("click", function handleClick(event) { if (event.target.tagName === "IMG") { imageSrc = event.target.src; document.body.removeEventListener("click", handleClick); markSelected("click-image"); event.preventDefault(); // Prevent default link behavior } }); } // Perform image search async function searchImage() { if (!file && imgType === "file") { showToast(lang("pleaseSelectFile"), "error"); return; } if (imgType === "file") { imageSrc = await getTmpImgLink(file); hideLoading(); // Hide loading animation after getting the link } if (!imageSrc) { return; } let tmp; if (selectedEngine !== "ascii2d") { tmp = encodeURIComponent(imageSrc); } else { tmp = imageSrc; } let target = searchUrl[selectedEngine].replace("${url}", tmp); GM_openInTab(target, { active: true, insert: true, setParent: true }); } /** * Asynchronously uploads a file to 0x0st and returns the uploaded URL. * * @param {File} file - The file to upload * @return {Promise} The URL of the uploaded file */ async function uploadTo0x0st(file) { const formData = new FormData(); formData.append("file", file); const response = new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://0x0.st", data: formData, headers: { "X-Client": "tampermonkey-enhanced-reverse-image-search", "User-Agent": "tampermonkey-enhanced-reverse-image-search", }, onload: function (response) { const data = response.responseText; if (data.startsWith("http")) { resolve(data.trim()); } else { reject(data); } }, onerror: function (response) { reject(response); }, }); }); return await response; } /** * Asynchronously uploads a file to file.io and returns the uploaded URL. * * @param {File} file - The file to upload * @return {Promise} The URL of the uploaded file */ async function uploadToFileIo(file) { const formData = new FormData(); formData.append("file", file); const response = new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://file.io", data: formData, headers: { "X-Client": "tampermonkey-enhanced-reverse-image-search", "User-Agent": "tampermonkey-enhanced-reverse-image-search", }, onload: function (response) { const data = JSON.parse(response.responseText); console.log("File.io response: "); console.log(data); if (data.success) { resolve(data.link); } else { reject(data.error); } }, onerror: function (response) { reject(response); }, }); }); return await response; } async function uploadToTempCloud(file){ const formData = new FormData(); formData.append("file", file); const response = new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url: "https://temp.kotol.cloud/api/file-upload?expiration=3600000", data: formData, headers: { "File-Name": file.name, "X-Client": "tampermonkey-enhanced-reverse-image-search", }, onload: function (response) { if (response.status !== 200) { reject(response.responseText); } const resp = JSON.parse(response.responseText); console.log("upload response: ", resp); resolve(`https://temp.kotol.cloud/api/download/${file.name}?code=${resp.code}`); }, onerror: function (response) { reject(response); }, }); }); return await response; } /** * 使用临时网盘来将文件转为链接搜图 * @param {*} file */ async function getTmpImgLink(file) { try { return await uploadToFileIo(file); } catch (error) { console.log("[reverse image search] upload error: ", error); showToast(lang("uploadError"), "error"); } } /** * Mark the selected image source with a green checkmark. * @param {string} id - The id of the selected image source. */ function markSelected(id) { imageSources.forEach((source) => { const element = document.getElementById(source.id); if (source.id === id) { element.style.fontWeight = "bold"; element.style.color = "green"; element.textContent = `✔ ${source.text}`; } else { element.style.fontWeight = "normal"; element.style.color = "black"; element.textContent = source.text; } }); } /** * Show loading animation * @param {HTMLElement} menu - The menu element */ function showLoading(menu) { const loadingDiv = document.createElement("div"); loadingDiv.id = "loading-animation"; loadingDiv.style.position = "absolute"; loadingDiv.style.top = "0"; loadingDiv.style.left = "0"; loadingDiv.style.width = "100%"; loadingDiv.style.height = "100%"; loadingDiv.style.backgroundColor = "rgba(255, 255, 255, 0.8)"; loadingDiv.style.display = "flex"; loadingDiv.style.justifyContent = "center"; loadingDiv.style.alignItems = "center"; loadingDiv.style.zIndex = "1000"; const spinner = document.createElement("div"); spinner.style.border = "4px solid #f3f3f3"; spinner.style.borderRadius = "50%"; spinner.style.borderTop = "4px solid #3498db"; spinner.style.width = "30px"; spinner.style.height = "30px"; spinner.style.animation = "spin 2s linear infinite"; loadingDiv.appendChild(spinner); menu.appendChild(loadingDiv); } /** * Hide loading animation */ function hideLoading() { const loadingDiv = document.getElementById("loading-animation"); if (loadingDiv) { loadingDiv.remove(); } } function showToast(message, type) { const toast = document.createElement("div"); toast.textContent = message; toast.style.position = "fixed"; toast.style.bottom = "20px"; toast.style.left = "50%"; toast.style.transform = "translateX(-50%)"; toast.style.backgroundColor = type === "success" ? "#4caf50" : "#f44336"; toast.style.color = "#fff"; toast.style.padding = "10px 20px"; toast.style.borderRadius = "5px"; toast.style.zIndex = "10000"; document.body.appendChild(toast); setTimeout(() => { toast.remove(); }, 3000); } function makeDraggable(element) { let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; element.onmousedown = dragMouseDown; function dragMouseDown(e) { e = e || window.event; e.preventDefault(); // get the mouse cursor position at startup: pos3 = e.clientX; pos4 = e.clientY; document.onmouseup = closeDragElement; // call a function whenever the cursor moves: document.onmousemove = elementDrag; } function elementDrag(e) { e = e || window.event; e.preventDefault(); // calculate the new cursor position: pos1 = pos3 - e.clientX; pos2 = pos4 - e.clientY; pos3 = e.clientX; pos4 = e.clientY; // set the element's new position: element.style.top = (element.offsetTop - pos2) + "px"; element.style.left = (element.offsetLeft - pos1) + "px"; } function closeDragElement() { // stop moving when mouse button is released: document.onmouseup = null; document.onmousemove = null; } } })();