您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Addon for X Spaces with custom emojis, enhanced transcript including mute/unmute, hand raise/lower, mic invites, join/leave events, and speaker queuing.
当前为
- // ==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();
- }
- })();