// ==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.58
// @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 reliably refreshes timeline by simulating home button click and verifying content update
// @description:zh-CN 添加圆形按钮,显示“顶”和“底”文字,居中于渐变背景,滚动到页面顶部/底部。Twitter (X) 首页点击“顶”按钮滚动顶部、显示圆环动画并通过模拟主页按钮点击和验证
// @author jiang
// @match https://x.com/*
// @match https://www.youtube.com/*
// @match https://m.youtube.com/*
// @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')) {
// Expanded selectors for Twitter home button
const refreshButton = document.querySelector(
'[href="/home"], [aria-label*="Home"], [data-testid="AppTabBar_Home_Link"], ' +
'[role="link"][href="/home"], [aria-label*="Timeline"], [data-testid*="home"], ' +
'[aria-label="Home timeline"], a[href="/home"]'
);
if (refreshButton) {
const spinner = showSpinner();
window.scrollTo({ top: 0, behavior: 'smooth' });
// Store initial top tweet ID to verify refresh
const initialTopTweet = document.querySelector('article[data-testid="tweet"]');
const initialTweetId = initialTopTweet ? initialTopTweet.querySelector('a[href*="/status/"]')?.href : null;
// Simulate click to refresh timeline
refreshButton.click();
// Verify content update
let attempts = 0;
const maxAttempts = 5;
const checkInterval = setInterval(() => {
const newTopTweet = document.querySelector('article[data-testid="tweet"]');
const newTweetId = newTopTweet ? newTopTweet.querySelector('a[href*="/status/"]')?.href : null;
if (newTweetId && newTweetId !== initialTweetId || attempts >= maxAttempts) {
clearInterval(checkInterval);
hideSpinner(spinner);
} else {
// Retry click if no update
refreshButton.click();
attempts++;
}
}, 1000);
} else {
console.log('RefreshAndScroll: Refresh button not found');
const spinner = showSpinner();
window.scrollTo({ top: 0, behavior: 'smooth' });
setTimeout(() => hideSpinner(spinner), 1500);
}
}
}
// 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 and layout logic
function customizeYouTubeLayout() {
if (!window.location.href.startsWith('https://www.youtube.com/') && !window.location.href.startsWith('https://m.youtube.com/')) return;
// Preloading logic
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:not([is-shorts])') ||
document.querySelector('ytd-video-renderer:last-of-type:not([is-shorts])') ||
document.querySelector('ytd-grid-video-renderer:last-of-type:not([is-shorts])');
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);
}
});
// Video layout and Shorts removal logic
const style = document.createElement('style');
style.textContent = `
#contents.ytd-rich-grid-renderer {
display: grid !important;
grid-template-columns: repeat(auto-fill, minmax(50%, 1fr)) !important;
gap: 10px !important;
padding: 10px !important;
box-sizing: border-box !important;
}
ytd-rich-item-renderer:not([is-shorts]), ytd-video-renderer:not([is-shorts]), ytd-grid-video-renderer:not([is-shorts]) {
height: ${window.innerHeight / 4}px !important;
margin: 0 !important;
overflow: hidden !important;
border-radius: 8px !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
}
ytd-rich-item-renderer:not([is-shorts]):nth-child(3n), ytd-video-renderer:not([is-shorts]):nth-child(3n), ytd-grid-video-renderer:not([is-shorts]):nth-child(3n) {
grid-column: span 2 !important;
width: 100% !important;
}
ytd-rich-item-renderer:not([is-shorts]) #thumbnail, ytd-video-renderer:not([is-shorts]) #thumbnail, ytd-grid-video-renderer:not([is-shorts]) #thumbnail {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
}
ytd-rich-item-renderer:not([is-shorts]) #details, ytd-video-renderer:not([is-shorts]) #details, ytd-grid-video-renderer:not([is-shorts]) #details {
display: none !important;
}
/* Shorts removal */
ytd-reel-shelf-renderer,
ytd-rich-shelf-renderer[is-shorts],
ytd-shorts,
ytd-reel-item-renderer,
ytd-rich-item-renderer[is-shorts],
ytd-video-renderer[is-shorts],
ytd-grid-video-renderer[is-shorts],
ytd-guide-entry-renderer:has(a[href*="/shorts"]),
ytd-mini-guide-entry-renderer:has(a[href*="/shorts"]),
ytm-pivot-bar-item-renderer:has(.pivot-shorts),
ytm-pivot-bar-item-renderer[tab-identifier="FEshorts"],
ytd-guide-entry-renderer:has([title="Shorts"]),
ytd-mini-guide-entry-renderer:has([title="Shorts"]),
ytm-rich-item-renderer:has([data-style="SHORTS"]),
ytm-reel-shelf-renderer,
ytm-rich-section-renderer:has(ytm-reel-shelf-renderer),
#chips-wrapper yt-chip-cloud-chip-renderer[chip-style*="STYLE_HOME_FILTER"]:has(a[href*="/shorts"]),
ytd-rich-section-renderer:has(#rich-shelf-header:contains("Shorts")),
ytd-item-section-renderer:has([overlay-style="SHORTS"]),
ytd-browse[page-subtype="shorts"],
ytd-rich-grid-row:empty,
#contents.ytd-rich-grid-row:empty {
display: none !important;
}
/* Fix grid layout after removing Shorts */
ytd-rich-grid-row, #contents.ytd-rich-grid-row {
display: contents !important;
}
`;
document.head.appendChild(style);
// Apply layout and remove Shorts dynamically
const applyLayoutAndRemoveShorts = () => {
// Apply video layout
const contents = document.querySelector('#contents.ytd-rich-grid-renderer');
if (contents) {
contents.style.display = 'grid';
const videos = document.querySelectorAll('ytd-rich-item-renderer:not([is-shorts]), ytd-video-renderer:not([is-shorts]), ytd-grid-video-renderer:not([is-shorts])');
videos.forEach((video, index) => {
video.style.height = `${window.innerHeight / 4}px`;
if ((index + 1) % 3 === 0) {
video.style.gridColumn = 'span 2';
video.style.width = '100%';
} else {
video.style.gridColumn = 'auto';
video.style.width = 'auto';
}
});
}
// Remove Shorts elements
const shortsSelectors = [
'ytd-reel-shelf-renderer',
'ytd-rich-shelf-renderer[is-shorts]',
'ytd-shorts',
'ytd-reel-item-renderer',
'ytd-rich-item-renderer[is-shorts]',
'ytd-video-renderer[is-shorts]',
'ytd-grid-video-renderer[is-shorts]',
'ytd-guide-entry-renderer:has(a[href*="/shorts"])',
'ytd-mini-guide-entry-renderer:has(a[href*="/shorts"])',
'ytm-pivot-bar-item-renderer:has(.pivot-shorts)',
'ytm-pivot-bar-item-renderer[tab-identifier="FEshorts"]',
'ytd-guide-entry-renderer:has([title="Shorts"])',
'ytd-mini-guide-entry-renderer:has([title="Shorts"])',
'ytm-rich-item-renderer:has([data-style="SHORTS"])',
'ytm-reel-shelf-renderer',
'ytm-rich-section-renderer:has(ytm-reel-shelf-renderer)',
'#chips-wrapper yt-chip-cloud-chip-renderer[chip-style*="STYLE_HOME_FILTER"]:has(a[href*="/shorts"])',
'ytd-rich-section-renderer:has(#rich-shelf-header:contains("Shorts"))',
'ytd-item-section-renderer:has([overlay-style="SHORTS"])',
'ytd-browse[page-subtype="shorts"]'
];
const shortsElements = document.querySelectorAll(shortsSelectors.join(','));
shortsElements.forEach(el => el.remove());
// Clean up empty grid rows
const emptyRows = document.querySelectorAll('ytd-rich-grid-row:empty, #contents.ytd-rich-grid-row:empty');
emptyRows.forEach(row => row.remove());
};
// Observe DOM changes to reapply layout and Shorts removal
const observer = new MutationObserver(() => {
applyLayoutAndRemoveShorts();
});
const target = document.body;
observer.observe(target, { childList: true, subtree: true });
// Redirect /shorts/ URLs to /watch?v=
const redirectShorts = () => {
if (window.location.href.includes('youtube.com/shorts/')) {
const newUrl = window.location.href.replace('/shorts/', '/watch?v=');
window.location.replace(newUrl);
}
};
redirectShorts();
window.addEventListener('popstate', redirectShorts);
// Initial application
applyLayoutAndRemoveShorts();
}
// 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 customizations
customizeYouTubeLayout();
} catch (err) {
console.log('RefreshAndScroll:', err);
}
})();