Image Search Script

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

目前为 2024-11-25 提交的版本,查看 最新版本

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