Trustee

Add a copy button to the profile header to copy the Roblox profile URL

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Trustee
// @namespace    https://github.com/pomsaddons/Trustee
// @version      1
// @description  Add a copy button to the profile header to copy the Roblox profile URL
// @match        https://www.roblox.com/users/*/profile*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js
// @license MIT 
// ==/UserScript==

(function () {
    'use strict';

    // Build a canonical profile URL similar to the extension's logic
    function buildUserProfileUrl(userId, isDeleted) {
        return `https://www.roblox.com/${isDeleted ? 'deleted-users' : 'users'}/${userId}/profile`;
    }

    function getMyUserId() {
        // Try meta tag first
        const meta = document.querySelector('meta[name="user-data"]');
        if (meta && meta.dataset && meta.dataset.userid) {
            return meta.dataset.userid;
        }
        // Fallback to global Roblox object if available
        if (window.Roblox && window.Roblox.CurrentUser) {
            return window.Roblox.CurrentUser.userId;
        }
        return null;
    }

    function getCsrfToken() {
        // Roblox stores the CSRF token in a meta tag
        const meta = document.querySelector('meta[name="csrf-token"]');
        return meta ? meta.getAttribute('data-token') : null;
    }

    async function fetchShareLink() {
        try {
            const csrfToken = getCsrfToken();
            if (!csrfToken) {
                console.warn('RoSeal Userscript: No CSRF token found');
                return null;
            }

            const resp = await fetch('https://apis.roblox.com/sharelinks/v1/get-or-create-link', {
                method: 'POST',
                credentials: 'include',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-TOKEN': csrfToken
                },
                body: JSON.stringify({
                    linkType: 'Profile'
                })
            });
            
            if (!resp.ok) {
                console.error(`RoSeal Userscript: API error ${resp.status}`);
                return null;
            }
            
            const json = await resp.json();
            return json.shortUrl;
        } catch (e) {
            console.error('RoSeal Userscript: Failed to get share link', e);
            return null;
        }
    }

    function showToast(text) {
        const id = 'roseal-copy-toast';
        let el = document.getElementById(id);
        if (!el) {
            el = document.createElement('div');
            el.id = id;
            Object.assign(el.style, {
                position: 'fixed',
                right: '16px',
                bottom: '16px',
                background: 'rgba(0,0,0,0.85)',
                color: 'white',
                padding: '8px 12px',
                borderRadius: '6px',
                fontSize: '13px',
                zIndex: 999999,
                boxShadow: '0 2px 8px rgba(0,0,0,0.4)'
            });
            document.body.appendChild(el);
        }
        el.textContent = text;
        el.style.opacity = '1';
        clearTimeout(el._rosealTimeout);
        el._rosealTimeout = setTimeout(() => {
            el.style.opacity = '0';
        }, 1800);
    }

    function showQrModal(url) {
        // Remove existing if any
        const existing = document.getElementById('roseal-qr-modal');
        if (existing) existing.remove();

        const overlay = document.createElement('div');
        overlay.id = 'roseal-qr-modal';
        Object.assign(overlay.style, {
            position: 'fixed',
            top: '0',
            left: '0',
            width: '100%',
            height: '100%',
            backgroundColor: 'rgba(0, 0, 0, 0.6)',
            zIndex: '999999',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            backdropFilter: 'blur(2px)'
        });

        const modal = document.createElement('div');
        const isDark = document.body.classList.contains('dark-theme');
        Object.assign(modal.style, {
            backgroundColor: isDark ? '#232527' : '#FFFFFF',
            color: isDark ? '#FFFFFF' : '#191919',
            padding: '24px',
            borderRadius: '12px',
            boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)',
            textAlign: 'center',
            maxWidth: '300px',
            position: 'relative',
            fontFamily: '"HCo Gotham SSm", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif'
        });

        // Close button
        const closeBtn = document.createElement('button');
        closeBtn.innerHTML = '×';
        Object.assign(closeBtn.style, {
            position: 'absolute',
            top: '8px',
            right: '12px',
            background: 'none',
            border: 'none',
            fontSize: '24px',
            cursor: 'pointer',
            color: 'inherit',
            opacity: '0.7'
        });
        closeBtn.onclick = () => overlay.remove();
        modal.appendChild(closeBtn);

        // QR Container
        const qrContainer = document.createElement('div');
        Object.assign(qrContainer.style, {
            margin: '16px auto',
            padding: '10px',
            backgroundColor: 'white', // QR codes need contrast
            borderRadius: '8px',
            width: 'fit-content'
        });
        modal.appendChild(qrContainer);

        // Generate QR
        // Using qrcode.js from @require
        try {
            new QRCode(qrContainer, {
                text: url,
                width: 180,
                height: 180,
                colorDark : "#000000",
                colorLight : "#ffffff",
                correctLevel : QRCode.CorrectLevel.H
            });
        } catch (e) {
            qrContainer.textContent = 'Error generating QR code. Please ensure script dependencies are loaded.';
            console.error(e);
        }

        // Text
        const text = document.createElement('p');
        text.textContent = 'Scan with your Roblox app in Connect > QR Code > Scan button in top right corner';
        Object.assign(text.style, {
            marginTop: '16px',
            fontSize: '14px',
            lineHeight: '1.4',
            opacity: '0.8'
        });
        modal.appendChild(text);

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        // Close on click outside
        overlay.addEventListener('click', (e) => {
            if (e.target === overlay) overlay.remove();
        });
    }

    function createSidebarButton(userId, isDeleted) {
        const li = document.createElement('li');
        const a = document.createElement('a');
        // Match Roblox sidebar classes
        a.className = 'dynamic-overflow-container text-nav roseal-copy-profile-sidebar-btn';
        a.href = '#';
        a.style.cursor = 'pointer';

        const divIcon = document.createElement('div');
        const spanIcon = document.createElement('span');
        // We'll use a custom class and inject CSS for the icon
        spanIcon.className = 'roseal-icon-copy';
        divIcon.appendChild(spanIcon);

        const spanText = document.createElement('span');
        spanText.className = 'font-header-2 dynamic-ellipsis-item';
        spanText.textContent = 'Profile QR Code';

        a.appendChild(divIcon);
        a.appendChild(spanText);
        li.appendChild(a);

        a.addEventListener('click', async (ev) => {
            ev.preventDefault();

            let urlToUse = buildUserProfileUrl(userId, isDeleted);
            const myId = getMyUserId();

            // If this is the current user, try to get the special share link
            if (userId && myId && String(userId) === String(myId) && !isDeleted) {
                showToast('Generating QR code...');
                const shareUrl = await fetchShareLink();
                if (shareUrl) {
                    urlToUse = shareUrl;
                }
            } else {
                showToast('Generating QR code...');
            }

            showQrModal(urlToUse);
        });

        return li;
    }

    function injectProfileButton() {
        // Check if already injected
        if (document.querySelector('.roseal-copy-profile-sidebar-btn')) return;

        // Find the sidebar list. It usually contains #nav-home or similar items.
        const navHome = document.getElementById('nav-home');
        const sidebarList = navHome ? navHome.closest('ul') : document.querySelector('.left-col-list ul');

        if (sidebarList) {
            // Extract user ID from URL
            const m = location.pathname.match(/\/users\/(\d+)\/profile/);
            if (m) {
                const userId = m[1];
                const isDeleted = false; 
                const btn = createSidebarButton(userId, isDeleted);
                
                // Append as the last li
                sidebarList.appendChild(btn);
            }
        }
    }

    // Initial scan
    injectProfileButton();

    // Observe DOM changes because Roblox is a SPA
    const mo = new MutationObserver((mutations) => {
        injectProfileButton();
    });

    mo.observe(document.body || document.documentElement, { childList: true, subtree: true });

    // Also use an interval for the first few seconds to catch late loads
    const interval = setInterval(() => {
        if (document.querySelector('.roseal-copy-profile-sidebar-btn')) {
            clearInterval(interval);
        } else {
            injectProfileButton();
        }
    }, 500);

    // Stop interval after 10 seconds
    setTimeout(() => clearInterval(interval), 10000);

    // Add CSS for the sidebar icon
    const style = document.createElement('style');
    style.textContent = `
        .roseal-icon-copy {
            display: inline-block;
            width: 28px;
            height: 28px;
            /* Simple copy icon SVG */
            background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23888'%3E%3Cpath d='M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z'/%3E%3C/svg%3E");
            background-size: 20px;
            background-repeat: no-repeat;
            background-position: center;
            opacity: 0.7;
        }
        /* Dark theme adjustment if needed, though #888 is usually okay for both */
        .dark-theme .roseal-icon-copy {
            background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23fff'%3E%3Cpath d='M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z'/%3E%3C/svg%3E");
        }
    `;
    document.head.appendChild(style);

})();