您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Refresh Twitter timeline by scrolling up at the top of the page
- // ==UserScript==
- // @name Twitter Scroll Refresh
- // @name:zh-CN Twitter 滚轮刷新
- // @namespace https://github.com/Xeron2000/twitter-scroll-refresh
- // @version 1.2.1
- // @description Refresh Twitter timeline by scrolling up at the top of the page
- // @description:zh-CN 在Twitter顶部向上滚动时自动刷新获取新帖子
- // @author Xeron
- // @match https://x.com/home
- // @icon https://abs.twimg.com/favicons/twitter.2.ico
- // @homepageURL https://github.com/Xeron2000/twitter-scroll-refresh
- // @supportURL https://github.com/Xeron2000/twitter-scroll-refresh/issues
- // @license MIT
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_registerMenuCommand
- // @run-at document-idle
- // ==/UserScript==
- /**
- * Twitter Scroll Refresh
- *
- * A userscript that allows refreshing Twitter/X timeline by scrolling up at the top.
- * 一个通过在顶部向上滚动来刷新Twitter/X时间线的用户脚本。
- *
- * @version 1.2.1
- * @author Xeron
- * @license MIT
- * @repository https://github.com/Xeron2000/twitter-scroll-refresh
- */
- (function() {
- 'use strict';
- // ====== Configuration / 配置 ======
- const CONFIG = {
- SCROLL_THRESHOLD: GM_getValue('scrollThreshold', 30), // 滚动阈值
- REFRESH_COOLDOWN: GM_getValue('refreshCooldown', 1500), // 刷新冷却时间(毫秒)
- TOP_OFFSET: GM_getValue('topOffset', 10), // 顶部偏移量
- SHOW_NOTIFICATIONS: GM_getValue('showNotifications', true), // 显示通知
- DEBUG_MODE: GM_getValue('debugMode', false), // 调试模式
- LANGUAGE: GM_getValue('language', 'auto') // 语言设置
- };
- // ====== Internationalization / 国际化 ======
- const MESSAGES = {
- en: {
- refreshTriggered: 'Refreshing timeline...',
- scrollToRefresh: 'Scroll up at top to refresh',
- settingsTitle: 'Twitter Scroll Refresh Settings',
- scrollThreshold: 'Scroll Threshold',
- refreshCooldown: 'Refresh Cooldown (ms)',
- topOffset: 'Top Offset (px)',
- showNotifications: 'Show Notifications',
- debugMode: 'Debug Mode',
- language: 'Language',
- languageAuto: 'Auto (Follow Browser)',
- languageEn: 'English',
- languageZhCn: '中文简体',
- save: 'Save',
- cancel: 'Cancel',
- saved: 'Settings saved!'
- },
- 'zh-CN': {
- refreshTriggered: '正在刷新时间线...',
- scrollToRefresh: '在顶部向上滚动可刷新',
- settingsTitle: 'Twitter滚轮刷新设置',
- scrollThreshold: '滚动阈值',
- refreshCooldown: '刷新冷却时间 (毫秒)',
- topOffset: '顶部偏移量 (像素)',
- showNotifications: '显示通知',
- debugMode: '调试模式',
- language: '语言',
- languageAuto: '自动 (跟随浏览器)',
- languageEn: 'English',
- languageZhCn: '中文简体',
- save: '保存',
- cancel: '取消',
- saved: '设置已保存!'
- }
- };
- // ====== Utility Functions / 工具函数 ======
- /**
- * Get current language
- * 获取当前语言
- */
- function getCurrentLanguage() {
- if (CONFIG.LANGUAGE !== 'auto') return CONFIG.LANGUAGE;
- return navigator.language.startsWith('zh') ? 'zh-CN' : 'en';
- }
- /**
- * Get localized message
- * 获取本地化消息
- */
- function getMessage(key) {
- const lang = getCurrentLanguage();
- return MESSAGES[lang]?.[key] || MESSAGES.en[key] || key;
- }
- /**
- * Debug logger
- * 调试日志
- */
- function debugLog(...args) {
- if (CONFIG.DEBUG_MODE) {
- console.log('[Twitter Scroll Refresh]', ...args);
- }
- }
- /**
- * Show notification
- * 显示通知
- */
- function showNotification(message, duration = 2000) {
- if (!CONFIG.SHOW_NOTIFICATIONS) return;
- // Remove existing notification
- const existing = document.getElementById('twitter-scroll-refresh-notification');
- if (existing) existing.remove();
- const notification = document.createElement('div');
- notification.id = 'twitter-scroll-refresh-notification';
- notification.style.cssText = `
- position: fixed;
- top: 20px;
- right: 20px;
- background: rgba(29, 161, 242, 0.95);
- color: white;
- padding: 12px 16px;
- border-radius: 8px;
- font-size: 14px;
- font-weight: 500;
- z-index: 10001;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
- backdrop-filter: blur(10px);
- opacity: 0;
- transform: translateX(100%);
- transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
- pointer-events: none;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- `;
- notification.textContent = message;
- document.body.appendChild(notification);
- // Animate in
- requestAnimationFrame(() => {
- notification.style.opacity = '1';
- notification.style.transform = 'translateX(0)';
- });
- // Auto remove
- setTimeout(() => {
- if (notification.parentNode) {
- notification.style.opacity = '0';
- notification.style.transform = 'translateX(100%)';
- setTimeout(() => {
- if (notification.parentNode) {
- notification.remove();
- }
- }, 300);
- }
- }, duration);
- }
- // ====== Main Logic / 主要逻辑 ======
- let isAtTop = false;
- let lastScrollTime = 0;
- let refreshing = false;
- let wheelStartTime = 0;
- /**
- * Check if page is at top
- * 检查是否在页面顶部
- */
- function checkIfAtTop() {
- return window.scrollY <= CONFIG.TOP_OFFSET;
- }
- /**
- * Find and execute refresh action
- * 查找并执行刷新操作
- */
- async function performRefresh() {
- if (refreshing) {
- debugLog('Refresh already in progress, skipping');
- return;
- }
- refreshing = true;
- debugLog('Refresh triggered');
- if (CONFIG.SHOW_NOTIFICATIONS) {
- showNotification(getMessage('refreshTriggered'));
- }
- try {
- // Method 1: Click Home tab if on home page
- // 方法1: 如果在主页则点击主页标签
- if (window.location.pathname === '/home') {
- const homeButton = document.querySelector('[data-testid="AppTabBar_Home_Link"]');
- if (homeButton) {
- debugLog('Clicking home button');
- homeButton.click();
- return;
- }
- }
- // Method 2: Look for refresh/reload buttons
- // 方法2: 查找刷新/重载按钮
- const refreshSelectors = [
- '[aria-label*="refresh" i]',
- '[aria-label*="reload" i]',
- '[aria-label*="刷新"]',
- '[aria-label*="重新加载"]',
- '[data-testid*="refresh"]',
- 'button[title*="refresh" i]',
- 'button[title*="刷新"]'
- ];
- for (const selector of refreshSelectors) {
- const button = document.querySelector(selector);
- if (button && button.offsetParent !== null) { // Check if visible
- debugLog('Clicking refresh button:', selector);
- button.click();
- return;
- }
- }
- // Method 3: Simulate '.' key press for timeline refresh
- // 方法3: 模拟按下'.'键刷新时间线
- debugLog('Using keyboard shortcut');
- const keyEvent = new KeyboardEvent('keydown', {
- key: '.',
- code: 'Period',
- keyCode: 190,
- which: 190,
- bubbles: true,
- cancelable: true
- });
- document.dispatchEvent(keyEvent);
- // Method 4: Look for "Show new posts" type buttons
- // 方法4: 查找"显示新帖子"类型的按钮
- setTimeout(() => {
- const newPostsSelectors = [
- '[role="button"]:has-text("Show")',
- '[role="button"]:has-text("显示")',
- 'button:contains("new")',
- 'button:contains("新")'
- ];
- // Use a more robust text search
- const buttons = document.querySelectorAll('button, [role="button"]');
- for (const button of buttons) {
- const text = button.textContent?.toLowerCase() || '';
- if ((text.includes('show') && text.includes('new')) ||
- (text.includes('显示') && text.includes('新'))) {
- debugLog('Clicking new posts button');
- button.click();
- return;
- }
- }
- }, 200);
- } catch (error) {
- debugLog('Error during refresh:', error);
- } finally {
- setTimeout(() => {
- refreshing = false;
- debugLog('Refresh cooldown completed');
- }, CONFIG.REFRESH_COOLDOWN);
- }
- }
- /**
- * Handle wheel event
- * 处理滚轮事件
- */
- function handleWheel(event) {
- const currentTime = Date.now();
- // Check if at top
- if (checkIfAtTop()) {
- if (!isAtTop) {
- isAtTop = true;
- wheelStartTime = currentTime;
- debugLog('Reached top of page');
- }
- // Check for upward scroll with sufficient velocity
- if (event.deltaY < -CONFIG.SCROLL_THRESHOLD) {
- // Prevent default scrolling when at top and scrolling up
- event.preventDefault();
- // Throttle refresh attempts
- if (currentTime - lastScrollTime > CONFIG.REFRESH_COOLDOWN) {
- debugLog('Upward scroll detected, triggering refresh');
- performRefresh();
- lastScrollTime = currentTime;
- }
- }
- } else {
- if (isAtTop) {
- isAtTop = false;
- debugLog('Left top of page');
- }
- }
- }
- /**
- * Handle scroll event
- * 处理滚动事件
- */
- function handleScroll() {
- const wasAtTop = isAtTop;
- isAtTop = checkIfAtTop();
- if (isAtTop !== wasAtTop) {
- debugLog('Top status changed:', isAtTop);
- }
- }
- // ====== Settings UI / 设置界面 ======
- /**
- * Create settings dialog
- * 创建设置对话框
- */
- function createSettingsDialog() {
- // Remove existing dialog
- const existing = document.getElementById('twitter-scroll-refresh-settings');
- if (existing) existing.remove();
- const dialog = document.createElement('div');
- dialog.id = 'twitter-scroll-refresh-settings';
- dialog.style.cssText = `
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.7);
- z-index: 10002;
- display: flex;
- align-items: center;
- justify-content: center;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- `;
- const content = document.createElement('div');
- content.style.cssText = `
- background: white;
- border-radius: 12px;
- padding: 24px;
- width: 420px;
- max-width: 90vw;
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
- `;
- const inputStyle = `
- width: 100%;
- padding: 10px 12px;
- border: 1px solid #e1e8ed;
- border-radius: 6px;
- font-size: 14px;
- color: #14171a;
- background: white;
- box-sizing: border-box;
- transition: border-color 0.2s ease;
- `;
- const inputFocusStyle = `
- outline: none;
- border-color: #1da1f2;
- box-shadow: 0 0 0 2px rgba(29, 161, 242, 0.1);
- `;
- content.innerHTML = `
- <h2 style="margin: 0 0 24px 0; color: #14171a; font-size: 20px; font-weight: 700; text-align: center;">
- ${getMessage('settingsTitle')}
- </h2>
- <form id="settings-form">
- <div style="margin-bottom: 18px;">
- <label style="display: block; margin-bottom: 8px; color: #657786; font-weight: 500; font-size: 13px;">
- ${getMessage('language')}
- </label>
- <select id="language" style="${inputStyle} cursor: pointer;">
- <option value="auto" ${CONFIG.LANGUAGE === 'auto' ? 'selected' : ''}>${getMessage('languageAuto')}</option>
- <option value="en" ${CONFIG.LANGUAGE === 'en' ? 'selected' : ''}>${getMessage('languageEn')}</option>
- <option value="zh-CN" ${CONFIG.LANGUAGE === 'zh-CN' ? 'selected' : ''}>${getMessage('languageZhCn')}</option>
- </select>
- </div>
- <div style="margin-bottom: 18px;">
- <label style="display: block; margin-bottom: 8px; color: #657786; font-weight: 500; font-size: 13px;">
- ${getMessage('scrollThreshold')}
- </label>
- <input type="number" id="scrollThreshold" value="${CONFIG.SCROLL_THRESHOLD}"
- style="${inputStyle}" min="10" max="100" step="5">
- </div>
- <div style="margin-bottom: 18px;">
- <label style="display: block; margin-bottom: 8px; color: #657786; font-weight: 500; font-size: 13px;">
- ${getMessage('refreshCooldown')}
- </label>
- <input type="number" id="refreshCooldown" value="${CONFIG.REFRESH_COOLDOWN}"
- style="${inputStyle}" min="500" max="5000" step="100">
- </div>
- <div style="margin-bottom: 18px;">
- <label style="display: block; margin-bottom: 8px; color: #657786; font-weight: 500; font-size: 13px;">
- ${getMessage('topOffset')}
- </label>
- <input type="number" id="topOffset" value="${CONFIG.TOP_OFFSET}"
- style="${inputStyle}" min="0" max="50" step="5">
- </div>
- <div style="margin-bottom: 18px; padding: 12px; background: #f7f9fa; border-radius: 8px;">
- <label style="display: flex; align-items: center; color: #14171a; font-weight: 500; cursor: pointer;">
- <input type="checkbox" id="showNotifications" ${CONFIG.SHOW_NOTIFICATIONS ? 'checked' : ''}
- style="margin-right: 10px; transform: scale(1.1);">
- ${getMessage('showNotifications')}
- </label>
- </div>
- <div style="margin-bottom: 24px; padding: 12px; background: #f7f9fa; border-radius: 8px;">
- <label style="display: flex; align-items: center; color: #14171a; font-weight: 500; cursor: pointer;">
- <input type="checkbox" id="debugMode" ${CONFIG.DEBUG_MODE ? 'checked' : ''}
- style="margin-right: 10px; transform: scale(1.1);">
- ${getMessage('debugMode')}
- </label>
- </div>
- <div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px;">
- <button type="button" id="cancel-btn" style="
- padding: 10px 20px;
- border: 1px solid #e1e8ed;
- background: white;
- color: #657786;
- border-radius: 6px;
- cursor: pointer;
- font-weight: 500;
- font-size: 14px;
- transition: all 0.2s ease;
- ">${getMessage('cancel')}</button>
- <button type="submit" id="save-btn" style="
- padding: 10px 20px;
- border: none;
- background: #1da1f2;
- color: white;
- border-radius: 6px;
- cursor: pointer;
- font-weight: 500;
- font-size: 14px;
- transition: all 0.2s ease;
- ">${getMessage('save')}</button>
- </div>
- </form>
- <style>
- #twitter-scroll-refresh-settings input:focus,
- #twitter-scroll-refresh-settings select:focus {
- ${inputFocusStyle}
- }
- #twitter-scroll-refresh-settings #cancel-btn:hover {
- background: #f7f9fa;
- border-color: #1da1f2;
- color: #1da1f2;
- }
- #twitter-scroll-refresh-settings #save-btn:hover {
- background: #1991da;
- }
- #twitter-scroll-refresh-settings select option {
- color: #14171a;
- background: white;
- padding: 8px;
- }
- </style>
- `;
- dialog.appendChild(content);
- document.body.appendChild(dialog);
- // Event handlers
- document.getElementById('cancel-btn').onclick = () => dialog.remove();
- document.getElementById('settings-form').onsubmit = (e) => {
- e.preventDefault();
- // Check if language changed
- const newLanguage = document.getElementById('language').value;
- const languageChanged = CONFIG.LANGUAGE !== newLanguage;
- // Save settings
- CONFIG.LANGUAGE = newLanguage;
- CONFIG.SCROLL_THRESHOLD = parseInt(document.getElementById('scrollThreshold').value);
- CONFIG.REFRESH_COOLDOWN = parseInt(document.getElementById('refreshCooldown').value);
- CONFIG.TOP_OFFSET = parseInt(document.getElementById('topOffset').value);
- CONFIG.SHOW_NOTIFICATIONS = document.getElementById('showNotifications').checked;
- CONFIG.DEBUG_MODE = document.getElementById('debugMode').checked;
- // Save to GM storage
- GM_setValue('language', CONFIG.LANGUAGE);
- GM_setValue('scrollThreshold', CONFIG.SCROLL_THRESHOLD);
- GM_setValue('refreshCooldown', CONFIG.REFRESH_COOLDOWN);
- GM_setValue('topOffset', CONFIG.TOP_OFFSET);
- GM_setValue('showNotifications', CONFIG.SHOW_NOTIFICATIONS);
- GM_setValue('debugMode', CONFIG.DEBUG_MODE);
- showNotification(getMessage('saved'));
- dialog.remove();
- // If language changed, suggest page refresh for full effect
- if (languageChanged) {
- setTimeout(() => {
- if (confirm(getCurrentLanguage() === 'zh-CN' ?
- '语言设置已更改,建议刷新页面以完全应用新语言设置。是否现在刷新?' :
- 'Language setting has been changed. It is recommended to refresh the page to fully apply the new language setting. Refresh now?')) {
- location.reload();
- }
- }, 1000);
- }
- };
- // Close on backdrop click
- dialog.onclick = (e) => {
- if (e.target === dialog) dialog.remove();
- };
- }
- // ====== Initialization / 初始化 ======
- /**
- * Initialize the script
- * 初始化脚本
- */
- function initialize() {
- debugLog('Initializing Twitter Scroll Refresh v1.2.1');
- // Add event listeners
- document.addEventListener('wheel', handleWheel, { passive: false });
- document.addEventListener('scroll', handleScroll, { passive: true });
- // Register menu command for settings
- GM_registerMenuCommand('⚙️ Settings / 设置', createSettingsDialog);
- // Show initialization notification
- setTimeout(() => {
- if (CONFIG.SHOW_NOTIFICATIONS) {
- showNotification(getMessage('scrollToRefresh'), 3000);
- }
- }, 1000);
- debugLog('Script initialized successfully');
- }
- // Wait for page to be ready
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', initialize);
- } else {
- initialize();
- }
- // Export for debugging (if needed)
- if (CONFIG.DEBUG_MODE) {
- window.TwitterScrollRefresh = {
- config: CONFIG,
- performRefresh,
- showSettings: createSettingsDialog
- };
- }
- })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址