IG Zoom Lens

Adds a circular zoom lens when hovering over images on Instagram, with panning support and zoom adjustment via mouse scroll.

تثبيت هذا البرنامج النصي؟
سكربت موصى به للمؤلف

ربما يعجبك أيضا NetSkip.

تثبيت هذا البرنامج النصي
// ==UserScript==
// @name               IG Zoom Lens
// @name:pt-BR         IG Zoom Lens
// @name:zh-CN         IG Zoom Lens
// @name:zh-TW         IG Zoom Lens
// @name:en            IG Zoom Lens
// @name:es            IG Zoom Lens
// @name:ja            IG Zoom Lens
// @name:ko            IG Zoom Lens
// @name:de            IG Zoom Lens
// @name:fr            IG Zoom Lens
// @namespace          http://github.com/0H4S
// @version            1.0
// @description        Adds a circular zoom lens when hovering over images on Instagram, with panning support and zoom adjustment via mouse scroll.
// @description:pt-BR  Adiciona uma lente de zoom circular ao passar o mouse sobre imagens no Instagram, com suporte a panning e ajuste de zoom via scroll do mouse.
// @description:zh-CN  在Instagram上悬停在图像上时添加一个圆形缩放镜头,支持平移和通过鼠标滚动调整缩放。
// @description:zh-TW  在Instagram上懸停在圖像上時添加一個圓形縮放鏡頭,支持平移和通過鼠標滾動調整縮放。
// @description:en     Adds a circular zoom lens when hovering over images on Instagram, with panning support and zoom adjustment via mouse scroll.
// @description:es     Añade una lente de zoom circular al pasar el ratón sobre las imágenes en Instagram, con soporte para paneo y ajuste de zoom mediante la rueda del ratón.
// @description:ja     Instagramの画像にマウスをホバーすると円形のズームレンズが追加され、パンニングサポートとマウススクロールによるズーム調整が可能になります。
// @description:ko     인스타그램 이미지 위에 마우스를 올리면 원형 줌 렌즈가 추가되며, 팬닝 지원 및 마우스 스크롤을 통한 줌 조정이 가능합니다.
// @description:de     Fügt eine kreisförmige Zoom-Linse hinzu, wenn Sie mit der Maus über Bilder auf Instagram fahren, mit Unterstützung für Panning und Zoom-Anpassung über das Mausrad.
// @description:fr     Ajoute une lentille de zoom circulaire lors du survol des images sur Instagram, avec prise en charge du panoramique et ajustement du zoom via la molette de la souris.
// @author             OHAS
// @license            CC-BY-NC-ND-4.0
// @copyright          2025 OHAS. All Rights Reserved.
// @icon               https://i.imgur.com/HH9zLZE.png
// @match              https://www.instagram.com/*
// @require            https://update.greasyfork.org/scripts/549920/Script%20Notifier.js
// @connect            gist.githubusercontent.com
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_registerMenuCommand
// @grant              GM_xmlhttpRequest
// @compatible         chrome
// @compatible         firefox
// @compatible         edge
// ==/UserScript==

(function() {
    'use strict';

    if (window.top !== window.self) {
        return;
    }

    const SCRIPT_CONFIG = {
        notificationsUrl: 'https://gist.githubusercontent.com/0H4S/aa1f90cc844db6c516379befd9ff354c/raw/ig_zoom_lens_notifications.json',
        scriptVersion: '1.0',
    };

    const notifier = new ScriptNotifier(SCRIPT_CONFIG);
    notifier.run();

    const DEFAULT_LENS_SIZE = 200;
    const MIN_LENS_SIZE = 100;
    const MAX_LENS_SIZE = 500;
    const LENS_SIZE_STEP = 20;
    const LENS_STORAGE_KEY = 'LensSize';

    const MIN_ZOOM = 3;
    const MAX_ZOOM = 30;
    const ZOOM_STEP = 0.5;
    const ZOOM_STORAGE_KEY = 'ZoomLevel';

    let currentLensSize = GM_getValue(LENS_STORAGE_KEY, DEFAULT_LENS_SIZE);
    let currentZoomLevel = GM_getValue(ZOOM_STORAGE_KEY, MIN_ZOOM);

    let currentImage = null;
    let isZoomActive = false;
    let isPanning = false;

    let panStartClientX = 0;
    let panStartClientY = 0;
    let initialBgPosX = 0;
    let initialBgPosY = 0;
    let lastMouseEvent = null;

    function addCursorStyles() {
        const style = document.createElement('style');
        style.textContent = ` .ig-zoom-active-image { cursor: none !important; } `;
        document.head.appendChild(style);
    }

    addCursorStyles();

    function createZoomLens() {
        const lens = document.createElement('div');
        lens.id = 'ig-zoom-lens';
        lens.style.position = 'fixed';
        lens.style.width = `${currentLensSize}px`;
        lens.style.height = `${currentLensSize}px`;
        lens.style.borderRadius = '50%';
        lens.style.border = '3px solid white';
        lens.style.boxShadow = '0 0 0 1px rgba(0,0,0,0.15), 0 4px 15px rgba(0,0,0,0.3)';
        lens.style.pointerEvents = 'none';
        lens.style.zIndex = '9999';
        lens.style.overflow = 'hidden';
        lens.style.transition = 'opacity 0.25s, transform 0.25s, border-color 0.15s linear';
        lens.style.backgroundRepeat = 'no-repeat';
        lens.style.backgroundSize = `${currentZoomLevel * 100}%`;
        lens.style.display = 'none';
        document.body.appendChild(lens);
        return lens;
    }

    const zoomLens = createZoomLens();

    function updateLensSize() {
        if (!zoomLens) return;
        zoomLens.style.width = `${currentLensSize}px`;
        zoomLens.style.height = `${currentLensSize}px`;
        if (isZoomActive && lastMouseEvent && currentImage) {
            updateZoomPosition(lastMouseEvent, currentImage);
        }
    }

    function provideVisualFeedback() {
        zoomLens.style.display = 'block';
        zoomLens.style.borderColor = '#3897f0';
        setTimeout(() => {
            zoomLens.style.borderColor = isPanning ? '#ff2323ff' : 'white';
            if (!isZoomActive) {
                zoomLens.style.display = 'none';
            }
        }, 200);
    }

    function increaseLensSize() {
        currentLensSize = Math.min(MAX_LENS_SIZE, currentLensSize + LENS_SIZE_STEP);
        GM_setValue(LENS_STORAGE_KEY, currentLensSize);
        updateLensSize();
        provideVisualFeedback();
    }

    function decreaseLensSize() {
        currentLensSize = Math.max(MIN_LENS_SIZE, currentLensSize - LENS_SIZE_STEP);
        GM_setValue(LENS_STORAGE_KEY, currentLensSize);
        updateLensSize();
        provideVisualFeedback();
    }

    function resetLensSize() {
        currentLensSize = DEFAULT_LENS_SIZE;
        GM_setValue(LENS_STORAGE_KEY, currentLensSize);
        updateLensSize();
        provideVisualFeedback();
    }

    document.addEventListener('keydown', function(e) {
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
            return;
        }

        if (e.key === '+' || e.key === '=') {
            e.preventDefault();
            increaseLensSize();
        } else if (e.key === '-') {
            e.preventDefault();
            decreaseLensSize();
        } else if (e.key === '0') {
            e.preventDefault();
            resetLensSize();
        }
    });

    function getHighResImageSrc(img) {
        if (img.srcset) {
            const sources = img.srcset.split(',').map(s => {
                const parts = s.trim().split(' ');
                return { url: parts[0], width: parseInt(parts[1]) || 0 };
            });
            sources.sort((a, b) => b.width - a.width);
            if (sources.length > 0 && sources[0].url) return sources[0].url;
        }
        return img.src;
    }

    function hideZoom() {
        if (!isZoomActive) return;
        isZoomActive = false;
        isPanning = false;
        if (currentImage) {
            currentImage.classList.remove('ig-zoom-active-image');
        }
        currentImage = null;
        zoomLens.style.borderColor = 'white';
        zoomLens.style.opacity = '0';
        zoomLens.style.transform = 'scale(0.8)';
        setTimeout(() => {
            if (!isZoomActive) {
                zoomLens.style.display = 'none';
            }
        }, 250);
    }

    function showZoom(img, e) {
        if (isZoomActive && currentImage === img) return;
        const src = getHighResImageSrc(img);
        if (!src || src.includes('profile_pic')) return;
        currentImage = img;
        isZoomActive = true;
        img.classList.add('ig-zoom-active-image');
        zoomLens.style.backgroundImage = `url(${src})`;
        zoomLens.style.backgroundSize = `${currentZoomLevel * 100}%`;
        zoomLens.style.display = 'block';
        setTimeout(() => {
            if (isZoomActive) {
                zoomLens.style.opacity = '1';
                zoomLens.style.transform = 'scale(1)';
            }
        }, 10);
        updateZoomPosition(e, img);
    }

    function updateZoomPosition(e, img) {
        if (!isZoomActive || currentImage !== img) return;
        lastMouseEvent = e;

        if (isPanning) {
            const deltaX = e.clientX - panStartClientX;
            const deltaY = e.clientY - panStartClientY;
            const deltaPercentX = (deltaX / currentLensSize) * 50;
            const deltaPercentY = (deltaY / currentLensSize) * 50;
            const newBgPosX = initialBgPosX + deltaPercentX;
            const newBgPosY = initialBgPosY + deltaPercentY;
            zoomLens.style.backgroundPosition = `${newBgPosX}% ${newBgPosY}%`;
            return;
        }

        const rect = img.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;

        if (x < 0 || y < 0 || x > rect.width || y > rect.height) {
            hideZoom();
            return;
        }

        const xPercent = (x / rect.width) * 100;
        const yPercent = (y / rect.height) * 100;

        zoomLens.style.left = `${e.clientX - currentLensSize / 2}px`;
        zoomLens.style.top = `${e.clientY - currentLensSize / 2}px`;
        zoomLens.style.backgroundPosition = `${xPercent}% ${yPercent}%`;
    }

    function applyZoomEffect(img) {
        if (img.dataset.zoomApplied) return;
        img.dataset.zoomApplied = 'true';

        img.addEventListener('mouseenter', function(e) { showZoom(img, e); });
        img.addEventListener('mousemove', function(e) { updateZoomPosition(e, img); });
        img.addEventListener('mouseleave', function() { if (!isPanning) { hideZoom(); } });

        img.addEventListener('mousedown', function(e) {
            if (e.button === 0 && isZoomActive) {
                e.preventDefault();
                isPanning = true;
                zoomLens.style.transition = 'none';
                zoomLens.style.borderColor = '#ff2323ff';
                panStartClientX = e.clientX;
                panStartClientY = e.clientY;
                const bgPos = zoomLens.style.backgroundPosition.split(' ');
                initialBgPosX = parseFloat(bgPos[0]) || 0;
                initialBgPosY = parseFloat(bgPos[1]) || 0;
            }
        });

        img.addEventListener('wheel', function(e) {
            if (!isZoomActive) return;
            e.preventDefault();
            const delta = e.deltaY < 0 ? ZOOM_STEP : -ZOOM_STEP;
            const newZoomLevel = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, currentZoomLevel + delta));
            if (newZoomLevel !== currentZoomLevel) {
                currentZoomLevel = newZoomLevel;
                GM_setValue(ZOOM_STORAGE_KEY, currentZoomLevel);
                zoomLens.style.backgroundSize = `${currentZoomLevel * 100}%`;
                if (!isPanning) {
                    updateZoomPosition(e, img);
                }
            }
        });
    }

    document.addEventListener('mouseup', function() {
        if (isPanning) {
            isPanning = false;
            zoomLens.style.transition = 'opacity 0.25s, transform 0.25s, border-color 0.15s linear';
            zoomLens.style.borderColor = 'white';
        }
    });

    function isOverNavigationButton(element) {
        if (!element) return false;
        let current = element;
        while (current && current !== document.body) {
            if (current.classList.contains('_afxw') || current.classList.contains('_afxv')) {
                return true;
            }
            current = current.parentElement;
        }
        return false;
    }

    document.addEventListener('mousemove', function(e) {
        lastMouseEvent = e;
        if (isPanning && currentImage) {
            updateZoomPosition(e, currentImage);
            return;
        }
        if (isOverNavigationButton(e.target) && isZoomActive) {
            hideZoom();
            return;
        }
        if (!e.target.closest('img') && isZoomActive) {
            hideZoom();
        }
    });

    function applyToAllImages() {
        const images = document.querySelectorAll('img.x5yr21d:not([src*="profile_pic"]):not([src*="s150x150"]):not([data-zoom-applied])');
        images.forEach(img => {
            if (img.offsetWidth > 150 || img.offsetHeight > 150) {
                applyZoomEffect(img);
            } else {
                img.onload = () => {
                    if (img.offsetWidth > 150 || img.offsetHeight > 150) {
                        applyZoomEffect(img);
                    }
                };
            }
        });
    }

    setTimeout(applyToAllImages, 1000);

    const observer = new MutationObserver(function(mutations) {
        let shouldReapply = false;
        mutations.forEach(mutation => {
            if (mutation.addedNodes.length) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.matches && node.matches('img.x5yr21d') || node.querySelector && node.querySelector('img.x5yr21d')) {
                            shouldReapply = true;
                        }
                    }
                });
            }
            if (mutation.type === 'attributes' && mutation.attributeName === 'src' && mutation.target.matches('img.x5yr21d')) {
                mutation.target.removeAttribute('data-zoom-applied');
                shouldReapply = true;
            }
        });
        if (shouldReapply) {
            setTimeout(applyToAllImages, 100);
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['src'],
        characterData: false
    });

})();