Image Search Script

长按滑鼠右键呼叫图片搜寻选单,提供流畅且简洁的使用体验。

目前为 2024-11-23 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Image Search Script
  3. // @name:zh-TW Image Search Script
  4. // @name:zh-CN Image Search Script
  5. // @namespace https://github.com/Pixmi/image-search-script
  6. // @version 1.1.8
  7. // @description Long-pressing the right mouse button brings up the image search menu, offering a smooth and concise user experience.
  8. // @description:zh-TW 長按滑鼠右鍵呼叫圖片搜尋選單,提供流暢且簡潔的使用體驗。
  9. // @description:zh-CN 长按滑鼠右键呼叫图片搜寻选单,提供流畅且简洁的使用体验。
  10. // @author Pixmi
  11. // @homepage https://github.com/Pixmi/image-search-script
  12. // @supportURL https://github.com/Pixmi/image-search-script/issues
  13. // @icon https://raw.githubusercontent.com/Pixmi/image-search-script/main/icon.svg
  14. // @match *://*/*
  15. // @require https://openuserjs.org/src/libs/sizzle/GM_config.js
  16. // @grant GM_setValue
  17. // @grant GM_getValue
  18. // @grant GM_addStyle
  19. // @grant GM_registerMenuCommand
  20. // @grant GM_xmlhttpRequest
  21. // @grant GM_openInTab
  22. // @connect ascii2d.net
  23. // @license GPL-3.0
  24. // @run-at document-body
  25. // @noframes
  26. // ==/UserScript==
  27.  
  28. GM_addStyle(`
  29. @keyframes fadeIn {
  30. 0% { opacity: 0; }
  31. 100% { opacity: 1; }
  32. }
  33. @keyframes fadeOut {
  34. 0% { opacity: 1; }
  35. 100% { opacity: 0; }
  36. }
  37. #image-search-menu {
  38. animation: fadeOut 200ms ease-in-out forwards;
  39. background-color: rgba(0, 0, 0, .75);
  40. color: rgb(255, 255, 255);
  41. display: none;
  42. flex-direction: column;
  43. font-size: 16px;
  44. width: unset;
  45. min-width: 150px;
  46. height: unset;
  47. min-height: unset;
  48. position: fixed;
  49. top: unset;
  50. left: unset;
  51. z-index: 9999;
  52. }
  53. #image-search-menu.show {
  54. animation: fadeIn 200ms ease-in-out forwards;
  55. display: flex;
  56. }
  57. .image-search-option {
  58. cursor: pointer;
  59. display: block;
  60. padding: 5px 10px;
  61. }
  62. .image-search-option + .image-search-option {
  63. border-top: 1px solid rgba(255, 255, 255, .5);
  64. }
  65. .image-search-option:hover {
  66. background-color: rgba(255, 255, 255, .3);
  67. }
  68. iframe#image-search-setting {
  69. width: 350px !important;
  70. height: 500px !important;
  71. }
  72. `);
  73.  
  74. const searchOptions = new Map([
  75. {
  76. label: 'Google Lens',
  77. key: 'GOOGLE_LENS',
  78. url: 'https://lens.google.com/uploadbyurl?url=%s'
  79. }, {
  80. label: 'SauceNAO',
  81. key: 'SAUCENAO',
  82. url: 'https://saucenao.com/search.php?url=%s'
  83. }, {
  84. label: 'Ascii2D',
  85. key: 'ASCII2D',
  86. url: ''
  87. }, {
  88. label: 'IQDB',
  89. key: 'IQDB',
  90. url: 'https://iqdb.org/?url=%s'
  91. }, {
  92. label: 'TinEye',
  93. key: 'TINEYE',
  94. url: 'https://www.tineye.com/search?url=%s'
  95. }, {
  96. label: 'Baidu',
  97. key: 'BAIDU',
  98. url: 'https://image.baidu.com/n/pc_search?queryImageUrl=%s'
  99. }, {
  100. label: 'Bing',
  101. key: 'BING',
  102. url: 'https://www.bing.com/images/searchbyimage?FORM=IRSBIQ&cbir=sbi&imgurl=%s'
  103. }
  104. ].map(item => [item.key, item]));
  105.  
  106. (function () {
  107. 'use strict';
  108.  
  109. const hoverOpen = {
  110. enabled: GM_getValue('HOVER_OPEN', false),
  111. minWidth: GM_getValue('HOVER_OPEN_MIN_WIDTH', 100),
  112. minHeight: GM_getValue('HOVER_OPEN_MIN_HEIGHT', 100)
  113. };
  114.  
  115. const newTab = (url) => {
  116. const tab = document.createElement('a');
  117. tab.href = url;
  118. tab.dispatchEvent(new MouseEvent('click', {ctrlKey: true, metaKey: true}));
  119. };
  120.  
  121. const hoverCheck = (event) => {
  122. const { target, relatedTarget } = event;
  123. if (target.className == 'image-search-option' && relatedTarget == searchMenu.image) {
  124. return true;
  125. }
  126. if (target.tagName === 'IMG' && target.width >= hoverOpen.minWidth && target.height >= hoverOpen.minHeight) {
  127. return true;
  128. }
  129. return false;
  130. };
  131.  
  132. document.addEventListener('mouseover', (event) => {
  133. if (hoverOpen.enabled) {
  134. if (hoverCheck(event)) {
  135. searchMenu.image = event.relatedTarget;
  136. searchMenu.open(event.target);
  137. } else {
  138. searchMenu.clear();
  139. }
  140. }
  141. });
  142.  
  143. document.addEventListener('mousedown', (event) => {
  144. searchMenu.holding = false;
  145. if (event.button === 2 && event.target.nodeName === 'IMG') {
  146. searchMenu.timer = setTimeout(() => {
  147. searchMenu.holding = true;
  148. searchMenu.open(event.target);
  149. }, 200);
  150. } else {
  151. if (event.target !== searchMenu.pane && !event.target.classList.contains('image-search-option')) {
  152. searchMenu.clear();
  153. }
  154. }
  155. });
  156.  
  157. document.addEventListener('mouseup', (event) => {
  158. if (event.button === 2) {
  159. clearTimeout(searchMenu.timer);
  160. if (searchMenu.holding) {
  161. event.preventDefault();
  162. }
  163. }
  164. });
  165.  
  166. document.addEventListener('contextmenu', (event) => {
  167. if (searchMenu.holding) {
  168. event.preventDefault();
  169. } else {
  170. searchMenu.clear();
  171. }
  172. });
  173.  
  174. document.addEventListener('visibilitychange', () => {
  175. if (document.visibilityState === 'visible') {
  176. searchMenu.update();
  177. }
  178. });
  179.  
  180. document.addEventListener('scroll', () => { searchMenu.update(); });
  181. window.addEventListener('resize', () => { searchMenu.update(); });
  182.  
  183. class searchMenuController {
  184. constructor() {
  185. this.panel = null;
  186. this.image = null;
  187. this.holding = false;
  188. this.timer = null;
  189. this.init();
  190. }
  191.  
  192. init() {
  193. this.panel = document.createElement('div');
  194. this.panel.id = 'image-search-menu';
  195. this.panel.addEventListener('click', (event) => {
  196. const action = event.target.dataset.action || false;
  197. if (action) {
  198. switch (action) {
  199. case 'ASCII2D':
  200. GM_xmlhttpRequest({
  201. method: 'POST',
  202. url: 'https://ascii2d.net/imagesearch/search/',
  203. data: JSON.stringify({ uri: this.image.src }),
  204. headers: {
  205. 'Content-Type': 'application/json',
  206. },
  207. timeout: 10000,
  208. onload: function(response) {
  209. if (response.status == 200) {
  210. newTab(response.finalUrl);
  211. }
  212. },
  213. onerror: function(error) {
  214. console.error('請求錯誤:', error);
  215. },
  216. ontimeout: function() {
  217. console.error('請求超時');
  218. }
  219. });
  220. break;
  221. default: {
  222. const option = searchOptions.get(action) || false;
  223. if (!option) break;
  224. const url = option.url.replace('%s', encodeURIComponent(this.image.src));
  225. GM_openInTab(url, true);
  226. break;
  227. }
  228. }
  229. }
  230. this.clear();
  231. });
  232. document.body.append(this.panel);
  233. }
  234.  
  235. open(target) {
  236. if (target.nodeName === 'IMG') {
  237. while (this.panel.hasChildNodes()) { this.panel.lastChild.remove(); }
  238. for (const [key, option] of searchOptions) {
  239. if (!GM_getValue(key, true)) continue;
  240. const item = document.createElement('div');
  241. item.className = 'image-search-option';
  242. item.textContent = option.label;
  243. item.dataset.action = key;
  244. this.panel.append((item));
  245. }
  246. this.image = target;
  247. this.update();
  248. this.panel.classList.add('show');
  249. }
  250. }
  251.  
  252. update() {
  253. if (this.image) {
  254. const status = {
  255. width: this.image.width,
  256. left: this.image.x,
  257. top: this.image.y
  258. };
  259. for (const key of Object.keys(status)) {
  260. this.panel.style[key] = `${status[key]}px`;
  261. }
  262. }
  263. }
  264.  
  265. clear() {
  266. this.image = null;
  267. this.panel.classList.remove('show');
  268. this.panel.style.width = 0;
  269. this.panel.style.left = 0;
  270. this.panel.style.top = 0;
  271. }
  272. }
  273.  
  274. const searchMenu = new searchMenuController();
  275.  
  276. GM_registerMenuCommand('Setting', () => config.open());
  277.  
  278. const config = new GM_config({
  279. 'id': 'image-search-setting',
  280. 'css': `
  281. #image-search-setting * {
  282. box-sizing: border-box;
  283. }
  284. #image-search-setting {
  285. box-sizing: border-box;
  286. width: 100%;
  287. height: 100%;
  288. padding: 10px;
  289. margin: 0;
  290. }
  291. #image-search-setting_wrapper {
  292. display: flex;
  293. flex-direction: column;
  294. height: 100%;
  295. }
  296. #image-search-setting_buttons_holder {
  297. text-align: center;
  298. margin-top: auto;
  299. }
  300. .config_var {
  301. margin: 5px 0 !important;
  302. }
  303. .field_label {
  304. font-size: 14px !important;
  305. }
  306. `,
  307. 'title': 'Image Search Setting',
  308. 'fields': {
  309. 'GOOGLE_LENS': {
  310. 'type': 'checkbox',
  311. 'label': 'Google Lens',
  312. 'section': ['Search Options'],
  313. 'default': true,
  314. },
  315. 'SAUCENAO': {
  316. 'type': 'checkbox',
  317. 'label': 'SauceNAO',
  318. 'default': true,
  319. },
  320. 'ASCII2D': {
  321. 'type': 'checkbox',
  322. 'label': 'Ascii2D',
  323. 'default': true,
  324. },
  325. 'IQDB': {
  326. 'type': 'checkbox',
  327. 'label': 'IQDB',
  328. 'default': true,
  329. },
  330. 'TINEYE': {
  331. 'type': 'checkbox',
  332. 'label': 'TinEye',
  333. 'default': true,
  334. },
  335. 'BAIDU': {
  336. 'type': 'checkbox',
  337. 'label': 'Baidu',
  338. 'default': true,
  339. },
  340. 'BING': {
  341. 'type': 'checkbox',
  342. 'label': 'Bing',
  343. 'default': true,
  344. },
  345. 'HOVER_OPEN': {
  346. 'type': 'checkbox',
  347. 'label': 'Enabled hover open',
  348. 'section': ['Hover images to open menu'],
  349. 'default': false,
  350. },
  351. 'HOVER_OPEN_MIN_WIDTH': {
  352. 'label': 'Image min width (px)',
  353. 'type': 'int',
  354. 'default': 100,
  355. },
  356. 'HOVER_OPEN_MIN_HEIGHT': {
  357. 'label': 'Image min height (px)',
  358. 'type': 'int',
  359. 'default': 100,
  360. }
  361. },
  362. 'events': {
  363. 'init': () => {
  364. for (const [key] of searchOptions) { config.set(key, GM_getValue(key, true)); }
  365. config.set('HOVER_OPEN', GM_getValue('HOVER_OPEN', false));
  366. config.set('HOVER_OPEN_MIN_WIDTH', GM_getValue('HOVER_OPEN_MIN_WIDTH', 100));
  367. config.set('HOVER_OPEN_MIN_HEIGHT', GM_getValue('HOVER_OPEN_MIN_HEIGHT', 100));
  368. },
  369. 'save': () => {
  370. for (const [key] of searchOptions) { GM_setValue(key, config.get(key)); }
  371. GM_setValue('HOVER_OPEN', config.get('HOVER_OPEN'));
  372. GM_setValue('HOVER_OPEN_MIN_WIDTH', config.get('HOVER_OPEN_MIN_WIDTH'));
  373. GM_setValue('HOVER_OPEN_MIN_HEIGHT', config.get('HOVER_OPEN_MIN_HEIGHT'));
  374. hoverOpen.enabled = config.get('HOVER_OPEN');
  375. hoverOpen.minWidth = config.get('HOVER_OPEN_MIN_WIDTH');
  376. hoverOpen.minHeight = config.get('HOVER_OPEN_MIN_HEIGHT');
  377. config.close();
  378. }
  379. }
  380. });
  381. })();