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 提交的版本。查看 最新版本

// ==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();
    }
})();