Twitter (X) & YouTube Refresh with Scroll Top/Bottom

Adds circular buttons with '顶' (top) and '底' (bottom) text, centered on gradient background, for scrolling to page top/bottom. On Twitter (X), top button scrolls up, shows ring animation, and refreshes timeline. On YouTube, preloads two screen heights of content ahead during any scroll for seamless scrolling, with same refresh animation. Other pages refresh with animation. Long press (300ms) to edit button positions, maintaining 60px spacing. Release to save; buttons fixed otherwise.

// ==UserScript==
// @name               Twitter (X) & YouTube Refresh with Scroll Top/Bottom
// @name:zh-CN         Twitter & YouTube v1
// @namespace          https://gist.github.com/4lrick/bedb39b069be0e4c94dc20214137c9f5
// @version            2.50
// @description        Adds circular buttons with '顶' (top) and '底' (bottom) text, centered on gradient background, for scrolling to page top/bottom. On Twitter (X), top button scrolls up, shows ring animation, and refreshes timeline. On YouTube, preloads two screen heights of content ahead during any scroll for seamless scrolling, with same refresh animation. Other pages refresh with animation. Long press (300ms) to edit button positions, maintaining 60px spacing. Release to save; buttons fixed otherwise.
// @description:zh-CN  添加圆形按钮,显示“顶”和“底”文字,居中于渐变背景,滚动到页面顶部/底部。Twitter (X) 首页点击“顶”按钮滚动顶部、显示圆环动画并刷新时间轴;YouTube 任意滑动时提前预加载两屏内容,确保无缝滚动,点击“顶”刷新页面;其他页面点击“顶”刷新并显示动画。长按300毫秒编辑按钮位置,保持60像素间距,松手保存,正常状态下按钮固定。
// @author             jiang
// @match              https://x.com/*
// @match              https://www.youtube.com/*
// @include            *
// @icon               https://www.google.com/s2/favicons?sz=64&domain=twitter.com
// @grant              GM_getValue
// @grant              GM_setValue
// @grant              GM_registerMenuCommand
// @grant              GM_unregisterMenuCommand
// @license            GPL-3.0-only
// ==/UserScript==

(function() {
    'use strict';

    // Prevent duplicate script execution
    const key = encodeURIComponent('RefreshAndScroll:执行判断');
    if (window[key]) { return; }
    window[key] = true;

    try {
        // Twitter (X) refresh interval configuration
        let refreshInterval = GM_getValue('refreshInterval', 5);
        let menuCommandId = null;

        // Display a loading spinner animation
        function showSpinner() {
            const spinner = document.createElement('div');
            spinner.id = 'refresh-spinner';
            spinner.innerHTML = `
                <div class="neon-ring"></div>
                <div class="neon-ring inner-ring" style="animation-delay: 0.1s;"></div>
                <div class="spark"></div>
                <div class="spark" style="animation-delay: 0.2s; transform: rotate(90deg);"></div>
                <div class="spark" style="animation-delay: 0.4s; transform: rotate(180deg);"></div>
                <div class="spark" style="animation-delay: 0.6s; transform: rotate(270deg);"></div>
            `;
            spinner.style.cssText = `
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                width: 100px;
                height: 100px;
                display: flex;
                justify-content: center;
                align-items: center;
                z-index: 10000;
            `;
            document.body.appendChild(spinner);
            return spinner;
        }

        // Remove the spinner animation
        function hideSpinner(spinner) {
            if (spinner) spinner.remove();
        }

        // Refresh Twitter (X) timeline
        function refreshTimeline() {
            if (window.location.href.startsWith('https://x.com/home')) {
                const refreshButton = document.querySelector('[href="/home"], [aria-label*="Home"], [data-testid="AppTabBar_Home_Link"]');
                if (refreshButton) {
                    const spinner = showSpinner();
                    window.scrollTo({ top: 0, behavior: 'smooth' });
                    refreshButton.click();
                    setTimeout(() => hideSpinner(spinner), 1500);
                } else {
                    console.log('RefreshAndScroll: Refresh button not found');
                }
            }
        }

        // Refresh non-Twitter pages (including YouTube)
        function refreshPage() {
            const spinner = showSpinner();
            window.scrollTo({ top: 0, behavior: 'smooth' });
            const reloadPromise = new Promise((resolve) => {
                window.addEventListener('load', resolve, { once: true });
                window.location.reload();
            });
            reloadPromise.then(() => {
                setTimeout(() => hideSpinner(spinner), 100);
            });
        }

        // YouTube preloading logic
        function preloadYouTubeContent() {
            if (!window.location.href.startsWith('https://www.youtube.com/')) return;

            const preloadThreshold = window.innerHeight * 2; // Two screen heights
            let isLoading = false;

            const scrollHandler = () => {
                if (isLoading) return;

                const scrollPosition = window.scrollY + window.innerHeight;
                const pageHeight = document.documentElement.scrollHeight;

                if (pageHeight - scrollPosition < preloadThreshold) {
                    isLoading = true;
                    const lastVideo = document.querySelector('ytd-rich-item-renderer:last-of-type') ||
                                     document.querySelector('ytd-video-renderer:last-of-type') ||
                                     document.querySelector('ytd-grid-video-renderer:last-of-type');
                    if (lastVideo) {
                        lastVideo.scrollIntoView({ behavior: 'instant' });
                        window.dispatchEvent(new Event('scroll'));
                        setTimeout(() => window.dispatchEvent(new Event('scroll')), 100);
                        setTimeout(() => window.dispatchEvent(new Event('scroll')), 200);
                        const observer = new MutationObserver(() => {
                            isLoading = false;
                            observer.disconnect();
                        });
                        const target = document.querySelector('#contents') || document.body;
                        observer.observe(target, { childList: true, subtree: true });
                        setTimeout(() => {
                            if (isLoading) {
                                isLoading = false;
                                observer.disconnect();
                            }
                        }, 5000);
                    } else {
                        isLoading = false;
                    }
                }
            };

            let isThrottled = false;
            window.addEventListener('scroll', () => {
                if (!isThrottled) {
                    isThrottled = true;
                    scrollHandler();
                    setTimeout(() => { isThrottled = false; }, 200);
                }
            });
        }

        // Set custom refresh interval for Twitter (X)
        function setCustomInterval() {
            const newInterval = prompt("Enter refresh interval in seconds:", refreshInterval);
            if (newInterval !== null) {
                const parsedInterval = parseInt(newInterval);
                if (!isNaN(parsedInterval) && parsedInterval > 0) {
                    refreshInterval = parsedInterval;
                    GM_setValue('refreshInterval', refreshInterval);
                    updateMenuCommand();
                } else {
                    alert("Please enter a valid positive number.");
                }
            }
        }

        // Update the menu command for refresh interval
        function updateMenuCommand() {
            if (menuCommandId) {
                GM_unregisterMenuCommand(menuCommandId);
            }
            menuCommandId = GM_registerMenuCommand(`Set Refresh Interval (current: ${refreshInterval}s)`, setCustomInterval);
        }

        // Scroll to top or bottom with conditional refresh
        function scrollToPosition(y, isTopButton = false) {
            window.scrollTo({ top: y, behavior: 'smooth' });
            if (y === 0) {
                if (window.location.href.startsWith('https://x.com/home')) {
                    setTimeout(() => refreshTimeline(), 500);
                } else if (isTopButton) {
                    setTimeout(() => refreshPage(), 500);
                }
            }
        }

        // Create a visual click effect
        function createClickEffect(x, y) {
            const effect = document.createElement('div');
            effect.style.cssText = `
                position: fixed;
                left: ${x}px;
                top: ${y}px;
                width: 10px;
                height: 10px;
                background: transparent;
                border: 2px solid #00ff88;
                border-radius: 50%;
                pointer-events: none;
                z-index: 10000;
                animation: shockwave 0.5s ease-out forwards;
                box-shadow: 0 0 10px #00ccff, 0 0 20px #00ff88;
            `;
            document.body.appendChild(effect);
            setTimeout(() => effect.remove(), 500);
        }

        // Inject CSS styles for buttons and animations
        const style = document.createElement('style');
        style.textContent = `
            @keyframes pulse {
                0% { transform: scale(1); }
                50% { transform: scale(1.1); }
                100% { transform: scale(1); }
            }
            @keyframes shockwave {
                0% {
                    transform: scale(1);
                    opacity: 1;
                    border-width: 2px;
                }
                100% {
                    transform: scale(10);
                    opacity: 0;
                    border-width: 0;
                }
            }
            @keyframes neonPulse {
                0% {
                    transform: scale(1) rotate(0deg);
                    opacity: 1;
                    box-shadow: 0 0 10px #00ff88, 0 0 20px #00ccff;
                }
                50% {
                    transform: scale(1.2) rotate(180deg);
                    opacity: 0.8;
                    box-shadow: 0 0 20px #00ff88, 0 0 40px #00ccff;
                }
                100% {
                    transform: scale(1) rotate(360deg);
                    opacity: 1;
                    box-shadow: 0 0 10px #00ff88, 0 0 20px #00ccff;
                }
            }
            @keyframes sparkBurst {
                0% {
                    transform: translate(0, 0) scale(1);
                    opacity: 1;
                }
                100% {
                    transform: translate(20px, 20px) scale(0);
                    opacity: 0;
                }
            }
            .neon-ring {
                position: absolute;
                width: 80px;
                height: 80px;
                border: 4px solid transparent;
                border-top-color: #00ff88;
                border-right-color: #00ccff;
                border-radius: 50%;
                animation: neonPulse 1.5s linear infinite;
            }
            .inner-ring {
                width: 60px;
                height: 60px;
                border-top-color: #00ccff;
                border-right-color: #00ff88;
                animation-direction: reverse;
            }
            .spark {
                position: absolute;
                width: 8px;
                height: 8px;
                background: #ffffff;
                border-radius: 50%;
                box-shadow: 0 0 10px #00ff88, 0 0 15px #00ccff;
                animation: sparkBurst 1.5s ease-out infinite;
            }
            #sky-scrolltop, #sky-scrolltbtm {
                font-family: 'Microsoft YaHei', 'Arial', sans-serif !important;
                font-style: normal;
                font-weight: 700;
                font-size: 16px;
                line-height: 48px !important;
                text-align: center !important;
                background: linear-gradient(135deg, #00ff88, #00ccff) !important;
                border-radius: 50% !important;
                width: 48px !important;
                height: 48px !important;
                color: #ffffff !important;
                cursor: pointer;
                position: fixed;
                z-index: 999999;
                user-select: none;
                box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
                transition: transform 0.2s ease, box-shadow 0.2s ease;
                animation: pulse 2s infinite;
                visibility: visible !important;
                display: flex !important;
                justify-content: center !important;
                align-items: center !important;
            }
            #sky-scrolltop:hover, #sky-scrolltbtm:hover {
                transform: scale(1.15);
                box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
            }
            #sky-scrolltop:active, #sky-scrolltbtm:active {
                transform: scale(0.95);
            }
            #sky-scrolltop.editing, #sky-scrolltbtm.editing {
                background: linear-gradient(135deg, #ff4444, #ff8888) !important;
                animation: none;
            }
        `;
        document.head.appendChild(style);

        // Create scroll buttons for top and bottom navigation
        const scrollTop = document.createElement('div');
        scrollTop.id = 'sky-scrolltop';
        scrollTop.innerText = '顶';
        scrollTop.setAttribute('data-text', '顶');
        scrollTop.style.visibility = 'visible';
        document.body.appendChild(scrollTop);
        console.log('RefreshAndScroll: Created top button with text:', scrollTop.innerText);

        const scrollBottom = document.createElement('div');
        scrollBottom.id = 'sky-scrolltbtm';
        scrollBottom.innerText = '底';
        scrollBottom.setAttribute('data-text', '底');
        scrollBottom.style.visibility = 'visible';
        document.body.appendChild(scrollBottom);
        console.log('RefreshAndScroll: Created bottom button with text:', scrollBottom.innerText);

        // Periodically check and restore button text
        setInterval(() => {
            const topButton = document.querySelector('#sky-scrolltop');
            const bottomButton = document.querySelector('#sky-scrolltbtm');
            if (topButton && topButton.innerText !== '顶') {
                console.error('RefreshAndScroll: Top button text missing, restoring...');
                topButton.innerText = topButton.getAttribute('data-text') || '顶';
            }
            if (bottomButton && bottomButton.innerText !== '底') {
                console.error('RefreshAndScroll: Bottom button text missing, restoring...');
                bottomButton.innerText = bottomButton.getAttribute('data-text') || '底';
            }
        }, 1000);

        // Initialize button positions
        let positions = GM_getValue('buttonPositions', { left: '20px', topBottom: '20%', bottomBottom: '12%' });
        scrollTop.style.left = positions.left;
        scrollTop.style.bottom = positions.topBottom;
        scrollBottom.style.left = positions.left;
        scrollBottom.style.bottom = positions.bottomBottom;

        // Position editing logic
        let isEditing = false;
        let startX, startY, initialLeft, initialBottomTop;
        const fixedSpacing = 60;

        // Start editing button positions on long press
        function startEditing(e) {
            e.preventDefault();
            isEditing = true;
            scrollTop.classList.add('editing');
            scrollBottom.classList.add('editing');
            startX = e.clientX || (e.touches && e.touches[0].clientX);
            startY = e.clientY || (e.touches && e.touches[0].clientY);
            initialLeft = parseFloat(scrollTop.style.left) || 20;
            initialBottomTop = parseFloat(scrollTop.style.bottom) || (window.innerHeight * 0.20);
            document.addEventListener('contextmenu', preventDefault, { capture: true });
            document.addEventListener('touchstart', preventDefault, { capture: true, passive: false });
        }

        // Move buttons during editing mode
        function moveButtons(e) {
            if (!isEditing) return;
            e.preventDefault();
            const clientX = e.clientX || (e.touches && e.touches[0].clientX);
            const clientY = e.clientY || (e.touches && e.touches[0].clientY);
            if (!clientX || !clientY) return;

            const deltaX = clientX - startX;
            const deltaY = startY - clientY;

            const buttonWidth = 48;
            const buttonHeight = 48;
            let newLeft = initialLeft + deltaX;
            let newBottomTop = initialBottomTop + deltaY;
            let newBottomBtm = newBottomTop - fixedSpacing;

            newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - buttonWidth));
            newBottomTop = Math.max(fixedSpacing, Math.min(newBottomTop, window.innerHeight - buttonHeight));
            newBottomBtm = newBottomTop - fixedSpacing;

            if (newBottomBtm >= 0) {
                scrollTop.style.left = `${newLeft}px`;
                scrollTop.style.bottom = `${newBottomTop}px`;
                scrollBottom.style.left = `${newLeft}px`;
                scrollBottom.style.bottom = `${newBottomBtm}px`;
            }
        }

        // Stop editing and save positions
        function stopEditing(e) {
            if (!isEditing) return;
            e.preventDefault();
            isEditing = false;
            scrollTop.classList.remove('editing');
            scrollBottom.classList.remove('editing');
            positions = {
                left: scrollTop.style.left,
                topBottom: scrollTop.style.bottom,
                bottomBottom: scrollBottom.style.bottom
            };
            GM_setValue('buttonPositions', positions);
            document.removeEventListener('contextmenu', preventDefault, { capture: true });
            document.removeEventListener('touchstart', preventDefault, { capture: true });
        }

        // Prevent default browser actions during editing
        function preventDefault(e) {
            e.preventDefault();
            e.stopPropagation();
        }

        // Long-press detection for editing mode
        let longPressTimer;
        const longPressDuration = 300;

        function handleLongPressStart(e) {
            clearTimeout(longPressTimer);
            longPressTimer = setTimeout(() => startEditing(e), longPressDuration);
        }

        function handleLongPressCancel() {
            clearTimeout(longPressTimer);
        }

        // Attach event listeners to buttons
        scrollTop.addEventListener('mousedown', handleLongPressStart);
        scrollTop.addEventListener('touchstart', handleLongPressStart, { passive: false });
        scrollTop.addEventListener('mouseup', handleLongPressCancel);
        scrollTop.addEventListener('mouseleave', handleLongPressCancel);
        scrollTop.addEventListener('touchend', handleLongPressCancel);
        scrollTop.addEventListener('click', (e) => {
            if (!isEditing) {
                const rect = scrollTop.getBoundingClientRect();
                createClickEffect(rect.left + rect.width / 2, rect.top + rect.height / 2);
                scrollToPosition(0, true);
            }
        });

        scrollBottom.addEventListener('mousedown', handleLongPressStart);
        scrollBottom.addEventListener('touchstart', handleLongPressStart, { passive: false });
        scrollBottom.addEventListener('mouseup', handleLongPressCancel);
        scrollBottom.addEventListener('mouseleave', handleLongPressCancel);
        scrollBottom.addEventListener('touchend', handleLongPressCancel);
        scrollBottom.addEventListener('click', (e) => {
            if (!isEditing) {
                const rect = scrollBottom.getBoundingClientRect();
                createClickEffect(rect.left + rect.width / 2, rect.top + rect.height / 2);
                scrollToPosition(document.body.scrollHeight);
            }
        });

        // Global event listeners for button movement
        document.addEventListener('mousemove', moveButtons);
        document.addEventListener('touchmove', moveButtons, { passive: false });
        document.addEventListener('mouseup', stopEditing);
        document.addEventListener('touchend', stopEditing);

        // Initialize Twitter (X) menu command
        if (window.location.href.startsWith('https://x.com/')) {
            updateMenuCommand();
        }

        // Initialize YouTube preloading
        preloadYouTubeContent();
    } catch (err) {
        console.log('RefreshAndScroll:', err);
    }
})();