Instagram Profile Picture Opener

Adds a styled context menu option to open Instagram profile pictures in new tab

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Instagram Profile Picture Opener
// @namespace    https://github.com/GooglyBlox
// @version      1.3
// @description  Adds a styled context menu option to open Instagram profile pictures in new tab
// @author       GooglyBlox
// @match        https://www.instagram.com/*
// @match        https://www.instagram.com/
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @grant        unsafeWindow
// @run-at       document-start
// @inject-into  content
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        .ig-custom-context-menu {
            position: fixed;
            z-index: 999999;
            background: rgba(255, 255, 255, 0.98);
            backdrop-filter: blur(12px);
            border: 1px solid rgba(219, 219, 219, 0.2);
            border-radius: 12px;
            box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
            padding: 8px;
            min-width: 240px;
            animation: menuFadeIn 0.2s ease-out;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
        }

        .ig-custom-context-menu-item {
            display: flex;
            align-items: center;
            padding: 12px 16px;
            color: #262626;
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            border-radius: 8px;
            transition: all 0.2s ease;
            gap: 12px;
        }

        .ig-custom-context-menu-item:hover {
            background-color: rgba(0, 0, 0, 0.05);
        }

        .ig-custom-context-menu-item svg {
            width: 20px;
            height: 20px;
            flex-shrink: 0;
        }

        @keyframes menuFadeIn {
            from {
                opacity: 0;
                transform: scale(0.95);
            }
            to {
                opacity: 1;
                transform: scale(1);
            }
        }

        .ig-custom-divider {
            height: 1px;
            background: rgba(219, 219, 219, 0.8);
            margin: 8px 4px;
        }

        @media (prefers-color-scheme: dark) {
            .ig-custom-context-menu {
                background: rgba(38, 38, 38, 0.98);
                border-color: rgba(38, 38, 38, 0.2);
            }

            .ig-custom-context-menu-item {
                color: #fafafa;
            }

            .ig-custom-context-menu-item:hover {
                background-color: rgba(255, 255, 255, 0.1);
            }

            .ig-custom-divider {
                background: rgba(38, 38, 38, 0.8);
            }
        }
    `);

    function getHDProfilePicUrl(url) {
        return url;
    }

    const icons = {
        openInNew: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
        </svg>`,
        download: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
        </svg>`
    };

    function handleContextMenu(event) {
        const img = event.target;

        if (img.tagName === 'IMG' && (
            img.closest('a[href^="/"]') !== null ||
            img.closest('[role="button"]') !== null ||
            img.closest('article') !== null
        )) {
            event.stopPropagation();
            event.preventDefault();

            const existingMenu = document.querySelector('.ig-custom-context-menu');
            if (existingMenu) {
                document.body.removeChild(existingMenu);
            }

            const contextMenu = document.createElement('div');
            contextMenu.className = 'ig-custom-context-menu';

            const rect = event.target.getBoundingClientRect();
            const x = event.clientX;
            const y = event.clientY;

            const menuWidth = 240;
            const menuHeight = 120;
            const viewportWidth = window.innerWidth;
            const viewportHeight = window.innerHeight;

            const adjustedX = Math.min(x, viewportWidth - menuWidth - 10);
            const adjustedY = Math.min(y, viewportHeight - menuHeight - 10);

            contextMenu.style.left = `${adjustedX}px`;
            contextMenu.style.top = `${adjustedY}px`;

            const openInNewTab = document.createElement('div');
            openInNewTab.className = 'ig-custom-context-menu-item';
            openInNewTab.innerHTML = `${icons.openInNew}Open Profile Picture`;

            openInNewTab.addEventListener('click', () => {
                const hdUrl = getHDProfilePicUrl(img.src);
                window.open(hdUrl, '_blank');
                document.body.removeChild(contextMenu);
            });

            const downloadImage = document.createElement('div');
            downloadImage.className = 'ig-custom-context-menu-item';
            downloadImage.innerHTML = `${icons.download}Download Profile Picture`;

            downloadImage.addEventListener('click', async () => {
                const hdUrl = getHDProfilePicUrl(img.src);
                const username = img.closest('a')?.getAttribute('href')?.replace(/\//g, '') || 'profile';

                try {
                    const response = await fetch(hdUrl);
                    const blob = await response.blob();
                    const blobUrl = window.URL.createObjectURL(blob);

                    const downloadLink = document.createElement('a');
                    downloadLink.href = blobUrl;
                    downloadLink.download = `${username}_profile_picture.jpg`;
                    downloadLink.click();

                    window.URL.revokeObjectURL(blobUrl);
                } catch (error) {
                    console.error('Failed to download image:', error);
                }

                document.body.removeChild(contextMenu);
            });

            contextMenu.appendChild(openInNewTab);
            contextMenu.appendChild(document.createElement('div')).className = 'ig-custom-divider';
            contextMenu.appendChild(downloadImage);

            document.body.appendChild(contextMenu);

            function removeContextMenu(e) {
                if (!contextMenu.contains(e.target)) {
                    document.body.removeChild(contextMenu);
                    document.removeEventListener('click', removeContextMenu);
                    document.removeEventListener('contextmenu', removeContextMenu);
                }
            }

            document.addEventListener('click', removeContextMenu);
            document.addEventListener('contextmenu', removeContextMenu);
        }
    }

    function init() {
        document.addEventListener('contextmenu', handleContextMenu, true);
    }

    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if (mutation.addedNodes.length) {
                init();
            }
        });
    });

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            init();
            observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        });
    } else {
        init();
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }
})();