Image Search Script

長按滑鼠右鍵,快速呼叫圖片搜尋選單,提供簡潔流暢的使用體驗。

目前為 2024-11-20 提交的版本,檢視 最新版本

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