Greasy Fork

Greasy Fork is available in English.

X Spaces + r

Addon for X Spaces with custom emojis, enhanced transcript including mute/unmute, hand raise/lower, mic invites, join/leave events, and speaker queuing.

当前为 2025-03-25 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         X Spaces + r 
// @namespace    Violentmonkey Scripts
// @version      1.91
// @description  Addon for X Spaces with custom emojis, enhanced transcript including mute/unmute, hand raise/lower, mic invites, join/leave events, and speaker queuing.
// @author       x.com/blankspeaker and x.com/PrestonHenshawX
// @match        https://twitter.com/*
// @match        https://x.com/*
// @run-at       document-start
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // [Previous unchanged code omitted for brevity: WebSocket, XMLHttpRequest, variables, fetchReplayUrl, debounce, getSpaceIdFromUrl, etc.]

    // [Keeping all other functions unchanged until formatTranscriptForDownload]

    async function formatTranscriptForDownload() {
        let transcriptText = '--- Space URLs ---\n';
        
        // Append live URL
        if (dynamicUrl) {
            transcriptText += `Live URL: ${dynamicUrl}\n`;
        } else {
            transcriptText += 'Live URL: Not available\n';
        }

        // Append replay URL (async fetch)
        try {
            const replayUrl = await fetchReplayUrl(dynamicUrl);
            transcriptText += `Replay URL: ${replayUrl}\n`;
        } catch (e) {
            transcriptText += 'Replay URL: Failed to generate\n';
        }

        transcriptText += '-----------------\n\n';

        let previousSpeaker = { username: '', handle: '' };
        const combinedData = [
            ...captionsData.map(item => ({ ...item, type: 'caption' })),
            ...emojiReactions.map(item => ({ ...item, type: 'emoji' }))
        ].sort((a, b) => a.timestamp - b.timestamp);

        combinedData.forEach((item, i) => {
            let { displayName, handle } = item;
            if (displayName === 'Unknown' && previousSpeaker.username) {
                displayName = previousSpeaker.username;
                handle = previousSpeaker.handle;
            }
            if (i > 0 && previousSpeaker.username !== displayName && item.type === 'caption') {
                transcriptText += '\n----------------------------------------\n';
            }
            if (item.type === 'caption') {
                transcriptText += `${displayName} ${handle}\n${item.text}\n\n`;
            } else if (item.type === 'emoji') {
                transcriptText += `${displayName} reacted with ${item.emoji}\n`;
            }
            previousSpeaker = { username: displayName, handle };
        });
        return transcriptText;
    }

    // [Unchanged functions: filterTranscript]

    function updateTranscriptPopup() {
        if (!transcriptPopup || transcriptPopup.style.display !== 'block') return;

        let queueContainer = transcriptPopup.querySelector('#queue-container');
        let searchContainer = transcriptPopup.querySelector('#search-container');
        let scrollArea = transcriptPopup.querySelector('#transcript-scrollable');
        let saveButton = transcriptPopup.querySelector('.save-button');
        let textSizeContainer = transcriptPopup.querySelector('.text-size-container');
        let systemToggleButton = transcriptPopup.querySelector('#system-toggle-button');
        let emojiToggleButton = transcriptPopup.querySelector('#emoji-toggle-button');
        let currentScrollTop = scrollArea ? scrollArea.scrollTop : 0;
        let wasAtBottom = scrollArea ? (scrollArea.scrollHeight - scrollArea.scrollTop - scrollArea.clientHeight < 50) : true;

        let showEmojis = localStorage.getItem(STORAGE_KEYS.SHOW_EMOJIS) !== 'false';
        let showSystemMessages = localStorage.getItem(STORAGE_KEYS.SHOW_SYSTEM_MESSAGES) !== 'false';

        if (!queueContainer || !searchContainer || !scrollArea || !saveButton || !textSizeContainer || !systemToggleButton || !emojiToggleButton) {
            transcriptPopup.innerHTML = '';

            queueContainer = document.createElement('div');
            queueContainer.id = 'queue-container';
            queueContainer.style.marginBottom = '10px';
            transcriptPopup.appendChild(queueContainer);

            searchContainer = document.createElement('div');
            searchContainer.id = 'search-container';
            searchContainer.style.display = 'none';
            searchContainer.style.marginBottom = '5px';

            const searchInput = document.createElement('input');
            searchInput.type = 'text';
            searchInput.placeholder = 'Search transcript...';
            searchInput.style.width = '87%';
            searchInput.style.padding = '5px';
            searchInput.style.backgroundColor = 'rgba(255, 255, 255, 0.1)';
            searchInput.style.border = 'none';
            searchInput.style.borderRadius = '5px';
            searchInput.style.color = 'white';
            searchInput.style.fontSize = '14px';
            searchInput.addEventListener('input', (e) => {
                searchTerm = e.target.value.trim();
                updateTranscriptPopup();
            });

            searchContainer.appendChild(searchInput);
            transcriptPopup.appendChild(searchContainer);

            scrollArea = document.createElement('div');
            scrollArea.id = 'transcript-scrollable';
            scrollArea.style.flex = '1';
            scrollArea.style.overflowY = 'auto';
            scrollArea.style.maxHeight = '300px';

            const captionWrapper = document.createElement('div');
            captionWrapper.id = 'transcript-output';
            captionWrapper.style.color = '#e7e9ea';
            captionWrapper.style.fontFamily = 'Arial, sans-serif';
            captionWrapper.style.whiteSpace = 'pre-wrap';
            captionWrapper.style.fontSize = `${currentFontSize}px`;
            scrollArea.appendChild(captionWrapper);

            const controlsContainer = document.createElement('div');
            controlsContainer.style.display = 'flex';
            controlsContainer.style.alignItems = 'center';
            controlsContainer.style.justifyContent = 'space-between';
            controlsContainer.style.padding = '5px 0';
            controlsContainer.style.borderTop = '1px solid rgba(255, 255, 255, 0.3)';

            saveButton = document.createElement('div');
            saveButton.className = 'save-button';
            saveButton.textContent = '💾 Save Transcript';
            saveButton.style.color = '#1DA1F2';
            saveButton.style.fontSize = '14px';
            saveButton.style.cursor = 'pointer';
            saveButton.addEventListener('click', async () => { // Updated to async
                saveButton.textContent = '💾 Saving...'; // Feedback during async operation
                const transcriptContent = await formatTranscriptForDownload();
                const blob = new Blob([transcriptContent], { type: 'text/plain' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = `transcript_${new Date().toISOString().replace(/[:.]/g, '-')}.txt`;
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                URL.revokeObjectURL(url);
                saveButton.textContent = '💾 Save Transcript'; // Reset button text
            });
            saveButton.addEventListener('mouseover', () => saveButton.style.color = '#FF9800');
            saveButton.addEventListener('mouseout', () => saveButton.style.color = '#1DA1F2');

            textSizeContainer = document.createElement('div');
            textSizeContainer.className = 'text-size-container';
            textSizeContainer.style.display = 'flex';
            textSizeContainer.style.alignItems = 'center';

            systemToggleButton = document.createElement('span');
            systemToggleButton.id = 'system-toggle-button';
            systemToggleButton.style.position = 'relative';
            systemToggleButton.style.fontSize = '14px';
            systemToggleButton.style.cursor = 'pointer';
            systemToggleButton.style.marginRight = '5px';
            systemToggleButton.style.width = '14px';
            systemToggleButton.style.height = '14px';
            systemToggleButton.style.display = 'inline-flex';
            systemToggleButton.style.alignItems = 'center';
            systemToggleButton.style.justifyContent = 'center';
            systemToggleButton.title = 'Toggle System Messages';
            systemToggleButton.innerHTML = '📢';

            const systemNotAllowedOverlay = document.createElement('span');
            systemNotAllowedOverlay.style.position = 'absolute';
            systemNotAllowedOverlay.style.width = '14px';
            systemNotAllowedOverlay.style.height = '14px';
            systemNotAllowedOverlay.style.border = '2px solid red';
            systemNotAllowedOverlay.style.borderRadius = '50%';
            systemNotAllowedOverlay.style.transform = 'rotate(45deg)';
            systemNotAllowedOverlay.style.background = 'transparent';
            systemNotAllowedOverlay.style.display = showSystemMessages ? 'none' : 'block';

            const systemSlash = document.createElement('span');
            systemSlash.style.position = 'absolute';
            systemSlash.style.width = '2px';
            systemSlash.style.height = '18px';
            systemSlash.style.background = 'red';
            systemSlash.style.transform = 'rotate(-45deg)';
            systemSlash.style.top = '-2px';
            systemSlash.style.left = '6px';
            systemNotAllowedOverlay.appendChild(systemSlash);

            systemToggleButton.appendChild(systemNotAllowedOverlay);

            systemToggleButton.addEventListener('click', () => {
                showSystemMessages = !showSystemMessages;
                systemNotAllowedOverlay.style.display = showSystemMessages ? 'none' : 'block';
                localStorage.setItem(STORAGE_KEYS.SHOW_SYSTEM_MESSAGES, showSystemMessages);
                updateTranscriptPopup();
            });

            emojiToggleButton = document.createElement('span');
            emojiToggleButton.id = 'emoji-toggle-button';
            emojiToggleButton.style.position = 'relative';
            emojiToggleButton.style.fontSize = '14px';
            emojiToggleButton.style.cursor = 'pointer';
            emojiToggleButton.style.marginRight = '5px';
            emojiToggleButton.style.width = '14px';
            emojiToggleButton.style.height = '14px';
            emojiToggleButton.style.display = 'inline-flex';
            emojiToggleButton.style.alignItems = 'center';
            emojiToggleButton.style.justifyContent = 'center';
            emojiToggleButton.title = 'Toggle Emoji Capturing';
            emojiToggleButton.innerHTML = '🙂';

            const emojiNotAllowedOverlay = document.createElement('span');
            emojiNotAllowedOverlay.style.position = 'absolute';
            emojiNotAllowedOverlay.style.width = '14px';
            emojiNotAllowedOverlay.style.height = '14px';
            emojiNotAllowedOverlay.style.border = '2px solid red';
            emojiNotAllowedOverlay.style.borderRadius = '50%';
            emojiNotAllowedOverlay.style.transform = 'rotate(45deg)';
            emojiNotAllowedOverlay.style.background = 'transparent';
            emojiNotAllowedOverlay.style.display = showEmojis ? 'none' : 'block';

            const emojiSlash = document.createElement('span');
            emojiSlash.style.position = 'absolute';
            emojiSlash.style.width = '2px';
            emojiSlash.style.height = '18px';
            emojiSlash.style.background = 'red';
            emojiSlash.style.transform = 'rotate(-45deg)';
            emojiSlash.style.top = '-2px';
            emojiSlash.style.left = '6px';
            emojiNotAllowedOverlay.appendChild(emojiSlash);

            emojiToggleButton.appendChild(emojiNotAllowedOverlay);

            emojiToggleButton.addEventListener('click', () => {
                showEmojis = !showEmojis;
                emojiNotAllowedOverlay.style.display = showEmojis ? 'none' : 'block';
                localStorage.setItem(STORAGE_KEYS.SHOW_EMOJIS, showEmojis);
                updateTranscriptPopup();
            });

            const magnifierEmoji = document.createElement('span');
            magnifierEmoji.textContent = '🔍';
            magnifierEmoji.style.marginRight = '5px';
            magnifierEmoji.style.fontSize = '14px';
            magnifierEmoji.style.cursor = 'pointer';
            magnifierEmoji.title = 'Search transcript';
            magnifierEmoji.addEventListener('click', () => {
                searchContainer.style.display = searchContainer.style.display === 'none' ? 'block' : 'none';
                if (searchContainer.style.display === 'block') searchInput.focus();
                else {
                    searchTerm = '';
                    searchInput.value = '';
                    updateTranscriptPopup();
                }
            });

            const textSizeSlider = document.createElement('input');
            textSizeSlider.type = 'range';
            textSizeSlider.min = '12';
            textSizeSlider.max = '18';
            textSizeSlider.value = currentFontSize;
            textSizeSlider.style.width = '50px';
            textSizeSlider.style.cursor = 'pointer';
            textSizeSlider.title = 'Adjust transcript text size';
            textSizeSlider.addEventListener('input', () => {
                currentFontSize = parseInt(textSizeSlider.value, 10);
                const captionWrapper = transcriptPopup.querySelector('#transcript-output');
                if (captionWrapper) captionWrapper.style.fontSize = `${currentFontSize}px`;
                localStorage.setItem('xSpacesCustomReactions_textSize', currentFontSize);
            });

            const savedTextSize = localStorage.getItem('xSpacesCustomReactions_textSize');
            if (savedTextSize) {
                currentFontSize = parseInt(savedTextSize, 10);
                textSizeSlider.value = currentFontSize;
            }

            textSizeContainer.appendChild(systemToggleButton);
            textSizeContainer.appendChild(emojiToggleButton);
            textSizeContainer.appendChild(magnifierEmoji);
            textSizeContainer.appendChild(textSizeSlider);

            controlsContainer.appendChild(saveButton);
            controlsContainer.appendChild(textSizeContainer);

            transcriptPopup.appendChild(queueContainer);
            transcriptPopup.appendChild(searchContainer);
            transcriptPopup.appendChild(scrollArea);
            transcriptPopup.appendChild(controlsContainer);
        }

        const { captions: filteredCaptions, emojis: filteredEmojis } = filterTranscript(captionsData, emojiReactions, searchTerm);
        const combinedData = [
            ...filteredCaptions.map(item => ({ ...item, type: 'caption' })),
            ...(showEmojis ? filteredEmojis.map(item => ({ ...item, type: 'emoji' })) : [])
        ].sort((a, b) => a.timestamp - b.timestamp);

        // Find the previous speaker before the last 200 entries
        let previousSpeaker = lastSpeaker || { username: '', handle: '' };
        if (combinedData.length > 200) {
            for (let i = combinedData.length - 201; i >= 0; i--) {
                if (combinedData[i].type === 'caption') {
                    previousSpeaker = { username: combinedData[i].displayName, handle: combinedData[i].handle };
                    break;
                }
            }
        }

        // Limit to the last 200 entries
        const recentData = combinedData.slice(-200);

        // Group consecutive emojis within the 200 entries
        let emojiGroups = [];
        let currentGroup = null;
        recentData.forEach(item => {
            if (item.type === 'caption') {
                if (currentGroup) {
                    emojiGroups.push(currentGroup);
                    currentGroup = null;
                }
                emojiGroups.push(item);
            } else if (item.type === 'emoji' && showEmojis) {
                if (currentGroup && currentGroup.displayName === item.displayName && currentGroup.emoji === item.emoji &&
                    Math.abs(item.timestamp - currentGroup.items[currentGroup.items.length - 1].timestamp) < 50) {
                    currentGroup.count++;
                    currentGroup.items.push(item);
                } else {
                    if (currentGroup) emojiGroups.push(currentGroup);
                    currentGroup = { displayName: item.displayName, emoji: item.emoji, count: 1, items: [item] };
                }
            }
        });
        if (currentGroup) emojiGroups.push(currentGroup);

        // Build the HTML string
        let html = '';
        if (combinedData.length > 200) {
            html += '<div style="color: #FFD700; font-size: 12px; margin-bottom: 10px;">Showing the last 200 lines. Save transcript to see the full conversation.</div>';
        }
        emojiGroups.forEach((group, i) => {
            if (group.type === 'caption') {
                let { displayName, handle, text } = group;
                if (displayName === 'Unknown' && previousSpeaker.username) {
                    displayName = previousSpeaker.username;
                    handle = previousSpeaker.handle;
                }
                if (i > 0 && previousSpeaker.username !== displayName) {
                    html += '<div style="border-top: 1px solid rgba(255, 255, 255, 0.3); margin: 5px 0;"></div>';
                }
                html += `<span style="font-size: ${currentFontSize}px; color: #1DA1F2">${displayName}</span> ` +
                        `<span style="font-size: ${currentFontSize}px; color: #808080">${handle}</span><br>` +
                        `<span style="font-size: ${currentFontSize}px; color: ${displayName === 'System' ? '#FF4500' : '#FFFFFF'}">${text}</span><br><br>`;
                previousSpeaker = { username: displayName, handle };
            } else if (showEmojis) {
                let { displayName, emoji, count } = group;
                if (displayName === 'Unknown' && previousSpeaker.username) {
                    displayName = previousSpeaker.username;
                }
                const countText = count > 1 ? ` <span style="font-size: ${currentFontSize}px; color: #FFD700">x${count}</span>` : '';
                html += `<span style="font-size: ${currentFontSize}px; color: #FFD700">${displayName}</span> ` +
                        `<span style="font-size: ${currentFontSize}px; color: #FFFFFF">reacted with ${emoji}${countText}</span><br>`;
                previousSpeaker = { username: displayName, handle: group.items[0].handle };
            }
        });

        // Update the DOM once
        const captionWrapper = scrollArea.querySelector('#transcript-output');
        if (captionWrapper) {
            captionWrapper.innerHTML = html;
            lastSpeaker = previousSpeaker;

            // Maintain scroll position
            if (wasAtBottom && !searchTerm) scrollArea.scrollTop = scrollArea.scrollHeight;
            else scrollArea.scrollTop = currentScrollTop;

            scrollArea.onscroll = () => {
                isUserScrolledUp = scrollArea.scrollHeight - scrollArea.scrollTop - scrollArea.clientHeight > 50;
            };
        }

        if (handQueuePopup && handQueuePopup.style.display === 'block') {
            updateHandQueueContent(handQueuePopup.querySelector('#hand-queue-content'));
        }
    }

    // [Unchanged functions: updateHandQueueContent, init, etc. omitted for brevity]

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();