Instagram Profile Picture Opener

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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
        });
    }
})();