Image Search Script

Long-pressing the right mouse button brings up the image search menu, offering a smooth and concise user experience.

// ==UserScript==
// @name        Image Search Script
// @name:zh-TW  Image Search Script
// @name:zh-CN  Image Search Script
// @namespace   https://github.com/Pixmi/image-search-script
// @version     1.1.9
// @description Long-pressing the right mouse button brings up the image search menu, offering a smooth and concise user experience.
// @description:zh-TW 長按滑鼠右鍵呼叫圖片搜尋選單,提供流暢且簡潔的使用體驗。
// @description:zh-CN 长按滑鼠右键呼叫图片搜寻选单,提供流畅且简洁的使用体验。
// @author      Pixmi
// @homepage    https://github.com/Pixmi/image-search-script
// @supportURL  https://github.com/Pixmi/image-search-script/issues
// @icon        https://github.com/Pixmi/image-search-script/raw/main/icon.svg
// @match       *://*/*
// @require     https://openuserjs.org/src/libs/sizzle/GM_config.js
// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_registerMenuCommand
// @grant       GM_xmlhttpRequest
// @grant       GM_openInTab
// @grant       GM_notification
// @connect     ascii2d.net
// @license     GPL-3.0
// @run-at      document-body
// @noframes
// ==/UserScript==

GM_addStyle(`
@keyframes fadeIn {
    0% { opacity: 0; }
    100% { opacity: 1; }
}
@keyframes fadeOut {
    0% { opacity: 1; }
    100% { opacity: 0; }
}
#image-search-menu {
    animation: fadeOut 200ms ease-in-out forwards;
    background-color: rgba(0, 0, 0, .75);
    color: rgb(255, 255, 255);
    display: none;
    flex-direction: column;
    font-size: 16px;
    width: unset;
    min-width: 150px;
    height: unset;
    min-height: unset;
    position: fixed;
    top: unset;
    left: unset;
    z-index: 9999;
}
#image-search-menu.show {
    animation: fadeIn 200ms ease-in-out forwards;
    display: flex;
}
.image-search-option {
    cursor: pointer;
    display: block;
    padding: 5px 10px;
}
.image-search-option + .image-search-option {
    border-top: 1px solid rgba(255, 255, 255, .5);
}
.image-search-option:hover {
    background-color: rgba(255, 255, 255, .3);
}
iframe#image-search-setting {
    width: 350px !important;
    height: 500px !important;
}
`);

const searchOptions = new Map([
    {
        label: 'Google Lens',
        key: 'GOOGLE_LENS',
        url: 'https://lens.google.com/uploadbyurl?url=%s'
    }, {
        label: 'SauceNAO',
        key: 'SAUCENAO',
        url: 'https://saucenao.com/search.php?url=%s'
    }, {
        label: 'Ascii2D',
        key: 'ASCII2D',
        url: ''
    }, {
        label: 'IQDB',
        key: 'IQDB',
        url: 'https://iqdb.org/?url=%s'
    }, {
        label: 'TinEye',
        key: 'TINEYE',
        url: 'https://www.tineye.com/search?url=%s'
    }, {
        label: 'Baidu',
        key: 'BAIDU',
        url: 'https://image.baidu.com/n/pc_search?queryImageUrl=%s'
    }, {
        label: 'Bing',
        key: 'BING',
        url: 'https://www.bing.com/images/searchbyimage?FORM=IRSBIQ&cbir=sbi&imgurl=%s'
    }
].map(item => [item.key, item]));

(function () {
    'use strict';

    const hoverOpen = {
        enabled: GM_getValue('HOVER_OPEN', false),
        minWidth: GM_getValue('HOVER_OPEN_MIN_WIDTH', 100),
        minHeight: GM_getValue('HOVER_OPEN_MIN_HEIGHT', 100)
    };

    const hoverCheck = (event) => {
        const { target, relatedTarget } = event;
        if (target.className == 'image-search-option' && relatedTarget == searchMenu.image) {
            return true;
        }
        if (target.tagName === 'IMG' && target.width >= hoverOpen.minWidth && target.height >= hoverOpen.minHeight) {
            return true;
        }
        return false;
    };

    const notey = (text) => {
        GM_notification(text, false, 'https://github.com/Pixmi/image-search-script/raw/main/icon.svg');
    };

    document.addEventListener('mouseover', (event) => {
        if (hoverOpen.enabled) {
            if (hoverCheck(event)) {
                searchMenu.image = event.relatedTarget;
                searchMenu.open(event.target);
            } else {
                searchMenu.clear();
            }
        }
    });

    document.addEventListener('mousedown', (event) => {
        searchMenu.holding = false;
        if (event.button === 2 && event.target.nodeName === 'IMG') {
            searchMenu.timer = setTimeout(() => {
                searchMenu.holding = true;
                searchMenu.open(event.target);
            }, 200);
        } else {
            if (event.target !== searchMenu.pane && !event.target.classList.contains('image-search-option')) {
                searchMenu.clear();
            }
        }
    });

    document.addEventListener('mouseup', (event) => {
        if (event.button === 2) {
            clearTimeout(searchMenu.timer);
            if (searchMenu.holding) {
                event.preventDefault();
            }
        }
    });

    document.addEventListener('contextmenu', (event) => {
        if (searchMenu.holding) {
            event.preventDefault();
        } else {
            searchMenu.clear();
        }
    });

    document.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'visible') {
            searchMenu.update();
        }
    });

    document.addEventListener('scroll', () => { searchMenu.update(); });
    window.addEventListener('resize', () => { searchMenu.update(); });

    class searchMenuController {
        constructor() {
            this.panel = null;
            this.image = null;
            this.holding = false;
            this.timer = null;
            this.init();
        }

        init() {
            this.panel = document.createElement('div');
            this.panel.id = 'image-search-menu';
            this.panel.addEventListener('click', (event) => {
                const action = event.target.dataset.action || false;
                if (action) {
                    switch (action) {
                        case 'ASCII2D':
                            GM_xmlhttpRequest({
                                method: 'POST',
                                url: 'https://ascii2d.net/imagesearch/search/',
                                data: JSON.stringify({ uri: this.image.src }),
                                headers: {
                                    'Content-Type': 'application/json',
                                },
                                timeout: 10000,
                                onload: function(response) {
                                    if (response.status == 200) {
                                        GM_openInTab(response.finalUrl, true);
                                    } else {
                                        notey('Ascii2D Response Error');
                                    }
                                },
                                onerror: function(error) {
                                    notey('Ascii2D Request Error');
                                },
                                ontimeout: function(error) {
                                    notey('Ascii2D Request Timeout');
                                }
                            });
                            break;
                        default: {
                            const option = searchOptions.get(action) || false;
                            if (!option) break;
                            const url = option.url.replace('%s', encodeURIComponent(this.image.src));
                            GM_openInTab(url, true);
                            break;
                        }
                    }
                }
                this.clear();
            });
            document.body.append(this.panel);
        }

        open(target) {
            if (target.nodeName === 'IMG') {
                while (this.panel.hasChildNodes()) { this.panel.lastChild.remove(); }
                for (const [key, option] of searchOptions) {
                    if (!GM_getValue(key, true)) continue;
                    const item = document.createElement('div');
                    item.className = 'image-search-option';
                    item.textContent = option.label;
                    item.dataset.action = key;
                    this.panel.append((item));
                }
                this.image = target;
                this.update();
                this.panel.classList.add('show');
            }
        }

        update() {
            if (this.image) {
                const status = {
                    width: this.image.width,
                    left: this.image.x,
                    top: this.image.y
                };
                for (const key of Object.keys(status)) {
                    this.panel.style[key] = `${status[key]}px`;
                }
            }
        }

        clear() {
            this.image = null;
            this.panel.classList.remove('show');
            this.panel.style.width = 0;
            this.panel.style.left = 0;
            this.panel.style.top = 0;
        }
    }

    const searchMenu = new searchMenuController();

    GM_registerMenuCommand('Setting', () => config.open());

    const config = new GM_config({
        'id': 'image-search-setting',
        'css': `
            #image-search-setting * {
                box-sizing: border-box;
            }
            #image-search-setting {
                box-sizing: border-box;
                width: 100%;
                height: 100%;
                padding: 10px;
                margin: 0;
            }
            #image-search-setting_wrapper {
                display: flex;
                flex-direction: column;
                height: 100%;
            }
            #image-search-setting_buttons_holder {
                text-align: center;
                margin-top: auto;
            }
            .config_var {
                margin: 5px 0 !important;
            }
            .field_label {
                font-size: 14px !important;
            }
        `,
        'title': 'Image Search Setting',
        'fields': {
            'GOOGLE_LENS': {
                'type': 'checkbox',
                'label': 'Google Lens',
                'section': ['Search Options'],
                'default': true,
            },
            'SAUCENAO': {
                'type': 'checkbox',
                'label': 'SauceNAO',
                'default': true,
            },
            'ASCII2D': {
                'type': 'checkbox',
                'label': 'Ascii2D',
                'default': true,
            },
            'IQDB': {
                'type': 'checkbox',
                'label': 'IQDB',
                'default': true,
            },
            'TINEYE': {
                'type': 'checkbox',
                'label': 'TinEye',
                'default': true,
            },
            'BAIDU': {
                'type': 'checkbox',
                'label': 'Baidu',
                'default': true,
            },
            'BING': {
                'type': 'checkbox',
                'label': 'Bing',
                'default': true,
            },
            'HOVER_OPEN': {
                'type': 'checkbox',
                'label': 'Enabled hover open',
                'section': ['Hover images to open menu'],
                'default': false,
            },
            'HOVER_OPEN_MIN_WIDTH': {
                'label': 'Image min width (px)',
                'type': 'int',
                'default': 100,
            },
            'HOVER_OPEN_MIN_HEIGHT': {
                'label': 'Image min height (px)',
                'type': 'int',
                'default': 100,
            }
        },
        'events': {
            'init': () => {
                for (const [key] of searchOptions) { config.set(key, GM_getValue(key, true)); }
                config.set('HOVER_OPEN', GM_getValue('HOVER_OPEN', false));
                config.set('HOVER_OPEN_MIN_WIDTH', GM_getValue('HOVER_OPEN_MIN_WIDTH', 100));
                config.set('HOVER_OPEN_MIN_HEIGHT', GM_getValue('HOVER_OPEN_MIN_HEIGHT', 100));
            },
            'save': () => {
                for (const [key] of searchOptions) { GM_setValue(key, config.get(key)); }
                GM_setValue('HOVER_OPEN', config.get('HOVER_OPEN'));
                GM_setValue('HOVER_OPEN_MIN_WIDTH', config.get('HOVER_OPEN_MIN_WIDTH'));
                GM_setValue('HOVER_OPEN_MIN_HEIGHT', config.get('HOVER_OPEN_MIN_HEIGHT'));
                hoverOpen.enabled = config.get('HOVER_OPEN');
                hoverOpen.minWidth = config.get('HOVER_OPEN_MIN_WIDTH');
                hoverOpen.minHeight = config.get('HOVER_OPEN_MIN_HEIGHT');
                config.close();
            }
        }
    });
})();