Twitter Scroll Refresh

Refresh Twitter timeline by scrolling up at the top of the page

  1. // ==UserScript==
  2. // @name Twitter Scroll Refresh
  3. // @name:zh-CN Twitter 滚轮刷新
  4. // @namespace https://github.com/Xeron2000/twitter-scroll-refresh
  5. // @version 1.2.1
  6. // @description Refresh Twitter timeline by scrolling up at the top of the page
  7. // @description:zh-CN 在Twitter顶部向上滚动时自动刷新获取新帖子
  8. // @author Xeron
  9. // @match https://x.com/home
  10. // @icon https://abs.twimg.com/favicons/twitter.2.ico
  11. // @homepageURL https://github.com/Xeron2000/twitter-scroll-refresh
  12. // @supportURL https://github.com/Xeron2000/twitter-scroll-refresh/issues
  13. // @license MIT
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // @grant GM_registerMenuCommand
  17. // @run-at document-idle
  18. // ==/UserScript==
  19.  
  20. /**
  21. * Twitter Scroll Refresh
  22. *
  23. * A userscript that allows refreshing Twitter/X timeline by scrolling up at the top.
  24. * 一个通过在顶部向上滚动来刷新Twitter/X时间线的用户脚本。
  25. *
  26. * @version 1.2.1
  27. * @author Xeron
  28. * @license MIT
  29. * @repository https://github.com/Xeron2000/twitter-scroll-refresh
  30. */
  31.  
  32. (function() {
  33. 'use strict';
  34.  
  35. // ====== Configuration / 配置 ======
  36. const CONFIG = {
  37. SCROLL_THRESHOLD: GM_getValue('scrollThreshold', 30), // 滚动阈值
  38. REFRESH_COOLDOWN: GM_getValue('refreshCooldown', 1500), // 刷新冷却时间(毫秒)
  39. TOP_OFFSET: GM_getValue('topOffset', 10), // 顶部偏移量
  40. SHOW_NOTIFICATIONS: GM_getValue('showNotifications', true), // 显示通知
  41. DEBUG_MODE: GM_getValue('debugMode', false), // 调试模式
  42. LANGUAGE: GM_getValue('language', 'auto') // 语言设置
  43. };
  44.  
  45. // ====== Internationalization / 国际化 ======
  46. const MESSAGES = {
  47. en: {
  48. refreshTriggered: 'Refreshing timeline...',
  49. scrollToRefresh: 'Scroll up at top to refresh',
  50. settingsTitle: 'Twitter Scroll Refresh Settings',
  51. scrollThreshold: 'Scroll Threshold',
  52. refreshCooldown: 'Refresh Cooldown (ms)',
  53. topOffset: 'Top Offset (px)',
  54. showNotifications: 'Show Notifications',
  55. debugMode: 'Debug Mode',
  56. language: 'Language',
  57. languageAuto: 'Auto (Follow Browser)',
  58. languageEn: 'English',
  59. languageZhCn: '中文简体',
  60. save: 'Save',
  61. cancel: 'Cancel',
  62. saved: 'Settings saved!'
  63. },
  64. 'zh-CN': {
  65. refreshTriggered: '正在刷新时间线...',
  66. scrollToRefresh: '在顶部向上滚动可刷新',
  67. settingsTitle: 'Twitter滚轮刷新设置',
  68. scrollThreshold: '滚动阈值',
  69. refreshCooldown: '刷新冷却时间 (毫秒)',
  70. topOffset: '顶部偏移量 (像素)',
  71. showNotifications: '显示通知',
  72. debugMode: '调试模式',
  73. language: '语言',
  74. languageAuto: '自动 (跟随浏览器)',
  75. languageEn: 'English',
  76. languageZhCn: '中文简体',
  77. save: '保存',
  78. cancel: '取消',
  79. saved: '设置已保存!'
  80. }
  81. };
  82.  
  83. // ====== Utility Functions / 工具函数 ======
  84.  
  85. /**
  86. * Get current language
  87. * 获取当前语言
  88. */
  89. function getCurrentLanguage() {
  90. if (CONFIG.LANGUAGE !== 'auto') return CONFIG.LANGUAGE;
  91. return navigator.language.startsWith('zh') ? 'zh-CN' : 'en';
  92. }
  93.  
  94. /**
  95. * Get localized message
  96. * 获取本地化消息
  97. */
  98. function getMessage(key) {
  99. const lang = getCurrentLanguage();
  100. return MESSAGES[lang]?.[key] || MESSAGES.en[key] || key;
  101. }
  102.  
  103. /**
  104. * Debug logger
  105. * 调试日志
  106. */
  107. function debugLog(...args) {
  108. if (CONFIG.DEBUG_MODE) {
  109. console.log('[Twitter Scroll Refresh]', ...args);
  110. }
  111. }
  112.  
  113. /**
  114. * Show notification
  115. * 显示通知
  116. */
  117. function showNotification(message, duration = 2000) {
  118. if (!CONFIG.SHOW_NOTIFICATIONS) return;
  119.  
  120. // Remove existing notification
  121. const existing = document.getElementById('twitter-scroll-refresh-notification');
  122. if (existing) existing.remove();
  123.  
  124. const notification = document.createElement('div');
  125. notification.id = 'twitter-scroll-refresh-notification';
  126. notification.style.cssText = `
  127. position: fixed;
  128. top: 20px;
  129. right: 20px;
  130. background: rgba(29, 161, 242, 0.95);
  131. color: white;
  132. padding: 12px 16px;
  133. border-radius: 8px;
  134. font-size: 14px;
  135. font-weight: 500;
  136. z-index: 10001;
  137. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  138. backdrop-filter: blur(10px);
  139. opacity: 0;
  140. transform: translateX(100%);
  141. transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  142. pointer-events: none;
  143. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  144. `;
  145. notification.textContent = message;
  146. document.body.appendChild(notification);
  147.  
  148. // Animate in
  149. requestAnimationFrame(() => {
  150. notification.style.opacity = '1';
  151. notification.style.transform = 'translateX(0)';
  152. });
  153.  
  154. // Auto remove
  155. setTimeout(() => {
  156. if (notification.parentNode) {
  157. notification.style.opacity = '0';
  158. notification.style.transform = 'translateX(100%)';
  159. setTimeout(() => {
  160. if (notification.parentNode) {
  161. notification.remove();
  162. }
  163. }, 300);
  164. }
  165. }, duration);
  166. }
  167.  
  168. // ====== Main Logic / 主要逻辑 ======
  169.  
  170. let isAtTop = false;
  171. let lastScrollTime = 0;
  172. let refreshing = false;
  173. let wheelStartTime = 0;
  174.  
  175. /**
  176. * Check if page is at top
  177. * 检查是否在页面顶部
  178. */
  179. function checkIfAtTop() {
  180. return window.scrollY <= CONFIG.TOP_OFFSET;
  181. }
  182.  
  183. /**
  184. * Find and execute refresh action
  185. * 查找并执行刷新操作
  186. */
  187. async function performRefresh() {
  188. if (refreshing) {
  189. debugLog('Refresh already in progress, skipping');
  190. return;
  191. }
  192.  
  193. refreshing = true;
  194. debugLog('Refresh triggered');
  195.  
  196. if (CONFIG.SHOW_NOTIFICATIONS) {
  197. showNotification(getMessage('refreshTriggered'));
  198. }
  199.  
  200. try {
  201. // Method 1: Click Home tab if on home page
  202. // 方法1: 如果在主页则点击主页标签
  203. if (window.location.pathname === '/home') {
  204. const homeButton = document.querySelector('[data-testid="AppTabBar_Home_Link"]');
  205. if (homeButton) {
  206. debugLog('Clicking home button');
  207. homeButton.click();
  208. return;
  209. }
  210. }
  211.  
  212. // Method 2: Look for refresh/reload buttons
  213. // 方法2: 查找刷新/重载按钮
  214. const refreshSelectors = [
  215. '[aria-label*="refresh" i]',
  216. '[aria-label*="reload" i]',
  217. '[aria-label*="刷新"]',
  218. '[aria-label*="重新加载"]',
  219. '[data-testid*="refresh"]',
  220. 'button[title*="refresh" i]',
  221. 'button[title*="刷新"]'
  222. ];
  223.  
  224. for (const selector of refreshSelectors) {
  225. const button = document.querySelector(selector);
  226. if (button && button.offsetParent !== null) { // Check if visible
  227. debugLog('Clicking refresh button:', selector);
  228. button.click();
  229. return;
  230. }
  231. }
  232.  
  233. // Method 3: Simulate '.' key press for timeline refresh
  234. // 方法3: 模拟按下'.'键刷新时间线
  235. debugLog('Using keyboard shortcut');
  236. const keyEvent = new KeyboardEvent('keydown', {
  237. key: '.',
  238. code: 'Period',
  239. keyCode: 190,
  240. which: 190,
  241. bubbles: true,
  242. cancelable: true
  243. });
  244. document.dispatchEvent(keyEvent);
  245.  
  246. // Method 4: Look for "Show new posts" type buttons
  247. // 方法4: 查找"显示新帖子"类型的按钮
  248. setTimeout(() => {
  249. const newPostsSelectors = [
  250. '[role="button"]:has-text("Show")',
  251. '[role="button"]:has-text("显示")',
  252. 'button:contains("new")',
  253. 'button:contains("新")'
  254. ];
  255.  
  256. // Use a more robust text search
  257. const buttons = document.querySelectorAll('button, [role="button"]');
  258. for (const button of buttons) {
  259. const text = button.textContent?.toLowerCase() || '';
  260. if ((text.includes('show') && text.includes('new')) ||
  261. (text.includes('显示') && text.includes('新'))) {
  262. debugLog('Clicking new posts button');
  263. button.click();
  264. return;
  265. }
  266. }
  267. }, 200);
  268.  
  269. } catch (error) {
  270. debugLog('Error during refresh:', error);
  271. } finally {
  272. setTimeout(() => {
  273. refreshing = false;
  274. debugLog('Refresh cooldown completed');
  275. }, CONFIG.REFRESH_COOLDOWN);
  276. }
  277. }
  278.  
  279. /**
  280. * Handle wheel event
  281. * 处理滚轮事件
  282. */
  283. function handleWheel(event) {
  284. const currentTime = Date.now();
  285.  
  286. // Check if at top
  287. if (checkIfAtTop()) {
  288. if (!isAtTop) {
  289. isAtTop = true;
  290. wheelStartTime = currentTime;
  291. debugLog('Reached top of page');
  292. }
  293.  
  294. // Check for upward scroll with sufficient velocity
  295. if (event.deltaY < -CONFIG.SCROLL_THRESHOLD) {
  296. // Prevent default scrolling when at top and scrolling up
  297. event.preventDefault();
  298.  
  299. // Throttle refresh attempts
  300. if (currentTime - lastScrollTime > CONFIG.REFRESH_COOLDOWN) {
  301. debugLog('Upward scroll detected, triggering refresh');
  302. performRefresh();
  303. lastScrollTime = currentTime;
  304. }
  305. }
  306. } else {
  307. if (isAtTop) {
  308. isAtTop = false;
  309. debugLog('Left top of page');
  310. }
  311. }
  312. }
  313.  
  314. /**
  315. * Handle scroll event
  316. * 处理滚动事件
  317. */
  318. function handleScroll() {
  319. const wasAtTop = isAtTop;
  320. isAtTop = checkIfAtTop();
  321.  
  322. if (isAtTop !== wasAtTop) {
  323. debugLog('Top status changed:', isAtTop);
  324. }
  325. }
  326.  
  327. // ====== Settings UI / 设置界面 ======
  328.  
  329. /**
  330. * Create settings dialog
  331. * 创建设置对话框
  332. */
  333. function createSettingsDialog() {
  334. // Remove existing dialog
  335. const existing = document.getElementById('twitter-scroll-refresh-settings');
  336. if (existing) existing.remove();
  337.  
  338. const dialog = document.createElement('div');
  339. dialog.id = 'twitter-scroll-refresh-settings';
  340. dialog.style.cssText = `
  341. position: fixed;
  342. top: 0;
  343. left: 0;
  344. right: 0;
  345. bottom: 0;
  346. background: rgba(0, 0, 0, 0.7);
  347. z-index: 10002;
  348. display: flex;
  349. align-items: center;
  350. justify-content: center;
  351. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  352. `;
  353.  
  354. const content = document.createElement('div');
  355. content.style.cssText = `
  356. background: white;
  357. border-radius: 12px;
  358. padding: 24px;
  359. width: 420px;
  360. max-width: 90vw;
  361. box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
  362. `;
  363.  
  364. const inputStyle = `
  365. width: 100%;
  366. padding: 10px 12px;
  367. border: 1px solid #e1e8ed;
  368. border-radius: 6px;
  369. font-size: 14px;
  370. color: #14171a;
  371. background: white;
  372. box-sizing: border-box;
  373. transition: border-color 0.2s ease;
  374. `;
  375.  
  376. const inputFocusStyle = `
  377. outline: none;
  378. border-color: #1da1f2;
  379. box-shadow: 0 0 0 2px rgba(29, 161, 242, 0.1);
  380. `;
  381.  
  382. content.innerHTML = `
  383. <h2 style="margin: 0 0 24px 0; color: #14171a; font-size: 20px; font-weight: 700; text-align: center;">
  384. ${getMessage('settingsTitle')}
  385. </h2>
  386. <form id="settings-form">
  387. <div style="margin-bottom: 18px;">
  388. <label style="display: block; margin-bottom: 8px; color: #657786; font-weight: 500; font-size: 13px;">
  389. ${getMessage('language')}
  390. </label>
  391. <select id="language" style="${inputStyle} cursor: pointer;">
  392. <option value="auto" ${CONFIG.LANGUAGE === 'auto' ? 'selected' : ''}>${getMessage('languageAuto')}</option>
  393. <option value="en" ${CONFIG.LANGUAGE === 'en' ? 'selected' : ''}>${getMessage('languageEn')}</option>
  394. <option value="zh-CN" ${CONFIG.LANGUAGE === 'zh-CN' ? 'selected' : ''}>${getMessage('languageZhCn')}</option>
  395. </select>
  396. </div>
  397. <div style="margin-bottom: 18px;">
  398. <label style="display: block; margin-bottom: 8px; color: #657786; font-weight: 500; font-size: 13px;">
  399. ${getMessage('scrollThreshold')}
  400. </label>
  401. <input type="number" id="scrollThreshold" value="${CONFIG.SCROLL_THRESHOLD}"
  402. style="${inputStyle}" min="10" max="100" step="5">
  403. </div>
  404. <div style="margin-bottom: 18px;">
  405. <label style="display: block; margin-bottom: 8px; color: #657786; font-weight: 500; font-size: 13px;">
  406. ${getMessage('refreshCooldown')}
  407. </label>
  408. <input type="number" id="refreshCooldown" value="${CONFIG.REFRESH_COOLDOWN}"
  409. style="${inputStyle}" min="500" max="5000" step="100">
  410. </div>
  411. <div style="margin-bottom: 18px;">
  412. <label style="display: block; margin-bottom: 8px; color: #657786; font-weight: 500; font-size: 13px;">
  413. ${getMessage('topOffset')}
  414. </label>
  415. <input type="number" id="topOffset" value="${CONFIG.TOP_OFFSET}"
  416. style="${inputStyle}" min="0" max="50" step="5">
  417. </div>
  418. <div style="margin-bottom: 18px; padding: 12px; background: #f7f9fa; border-radius: 8px;">
  419. <label style="display: flex; align-items: center; color: #14171a; font-weight: 500; cursor: pointer;">
  420. <input type="checkbox" id="showNotifications" ${CONFIG.SHOW_NOTIFICATIONS ? 'checked' : ''}
  421. style="margin-right: 10px; transform: scale(1.1);">
  422. ${getMessage('showNotifications')}
  423. </label>
  424. </div>
  425. <div style="margin-bottom: 24px; padding: 12px; background: #f7f9fa; border-radius: 8px;">
  426. <label style="display: flex; align-items: center; color: #14171a; font-weight: 500; cursor: pointer;">
  427. <input type="checkbox" id="debugMode" ${CONFIG.DEBUG_MODE ? 'checked' : ''}
  428. style="margin-right: 10px; transform: scale(1.1);">
  429. ${getMessage('debugMode')}
  430. </label>
  431. </div>
  432. <div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px;">
  433. <button type="button" id="cancel-btn" style="
  434. padding: 10px 20px;
  435. border: 1px solid #e1e8ed;
  436. background: white;
  437. color: #657786;
  438. border-radius: 6px;
  439. cursor: pointer;
  440. font-weight: 500;
  441. font-size: 14px;
  442. transition: all 0.2s ease;
  443. ">${getMessage('cancel')}</button>
  444. <button type="submit" id="save-btn" style="
  445. padding: 10px 20px;
  446. border: none;
  447. background: #1da1f2;
  448. color: white;
  449. border-radius: 6px;
  450. cursor: pointer;
  451. font-weight: 500;
  452. font-size: 14px;
  453. transition: all 0.2s ease;
  454. ">${getMessage('save')}</button>
  455. </div>
  456. </form>
  457. <style>
  458. #twitter-scroll-refresh-settings input:focus,
  459. #twitter-scroll-refresh-settings select:focus {
  460. ${inputFocusStyle}
  461. }
  462. #twitter-scroll-refresh-settings #cancel-btn:hover {
  463. background: #f7f9fa;
  464. border-color: #1da1f2;
  465. color: #1da1f2;
  466. }
  467. #twitter-scroll-refresh-settings #save-btn:hover {
  468. background: #1991da;
  469. }
  470. #twitter-scroll-refresh-settings select option {
  471. color: #14171a;
  472. background: white;
  473. padding: 8px;
  474. }
  475. </style>
  476. `;
  477.  
  478. dialog.appendChild(content);
  479. document.body.appendChild(dialog);
  480.  
  481. // Event handlers
  482. document.getElementById('cancel-btn').onclick = () => dialog.remove();
  483. document.getElementById('settings-form').onsubmit = (e) => {
  484. e.preventDefault();
  485.  
  486. // Check if language changed
  487. const newLanguage = document.getElementById('language').value;
  488. const languageChanged = CONFIG.LANGUAGE !== newLanguage;
  489.  
  490. // Save settings
  491. CONFIG.LANGUAGE = newLanguage;
  492. CONFIG.SCROLL_THRESHOLD = parseInt(document.getElementById('scrollThreshold').value);
  493. CONFIG.REFRESH_COOLDOWN = parseInt(document.getElementById('refreshCooldown').value);
  494. CONFIG.TOP_OFFSET = parseInt(document.getElementById('topOffset').value);
  495. CONFIG.SHOW_NOTIFICATIONS = document.getElementById('showNotifications').checked;
  496. CONFIG.DEBUG_MODE = document.getElementById('debugMode').checked;
  497.  
  498. // Save to GM storage
  499. GM_setValue('language', CONFIG.LANGUAGE);
  500. GM_setValue('scrollThreshold', CONFIG.SCROLL_THRESHOLD);
  501. GM_setValue('refreshCooldown', CONFIG.REFRESH_COOLDOWN);
  502. GM_setValue('topOffset', CONFIG.TOP_OFFSET);
  503. GM_setValue('showNotifications', CONFIG.SHOW_NOTIFICATIONS);
  504. GM_setValue('debugMode', CONFIG.DEBUG_MODE);
  505.  
  506. showNotification(getMessage('saved'));
  507. dialog.remove();
  508.  
  509. // If language changed, suggest page refresh for full effect
  510. if (languageChanged) {
  511. setTimeout(() => {
  512. if (confirm(getCurrentLanguage() === 'zh-CN' ?
  513. '语言设置已更改,建议刷新页面以完全应用新语言设置。是否现在刷新?' :
  514. 'Language setting has been changed. It is recommended to refresh the page to fully apply the new language setting. Refresh now?')) {
  515. location.reload();
  516. }
  517. }, 1000);
  518. }
  519. };
  520.  
  521. // Close on backdrop click
  522. dialog.onclick = (e) => {
  523. if (e.target === dialog) dialog.remove();
  524. };
  525. }
  526.  
  527. // ====== Initialization / 初始化 ======
  528.  
  529. /**
  530. * Initialize the script
  531. * 初始化脚本
  532. */
  533. function initialize() {
  534. debugLog('Initializing Twitter Scroll Refresh v1.2.1');
  535.  
  536. // Add event listeners
  537. document.addEventListener('wheel', handleWheel, { passive: false });
  538. document.addEventListener('scroll', handleScroll, { passive: true });
  539.  
  540. // Register menu command for settings
  541. GM_registerMenuCommand('⚙️ Settings / 设置', createSettingsDialog);
  542.  
  543. // Show initialization notification
  544. setTimeout(() => {
  545. if (CONFIG.SHOW_NOTIFICATIONS) {
  546. showNotification(getMessage('scrollToRefresh'), 3000);
  547. }
  548. }, 1000);
  549.  
  550. debugLog('Script initialized successfully');
  551. }
  552.  
  553. // Wait for page to be ready
  554. if (document.readyState === 'loading') {
  555. document.addEventListener('DOMContentLoaded', initialize);
  556. } else {
  557. initialize();
  558. }
  559.  
  560. // Export for debugging (if needed)
  561. if (CONFIG.DEBUG_MODE) {
  562. window.TwitterScrollRefresh = {
  563. config: CONFIG,
  564. performRefresh,
  565. showSettings: createSettingsDialog
  566. };
  567. }
  568.  
  569. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址