Twitter & YouTube v1

添加圆形按钮,显示“顶”和“底”文字,居中于渐变背景,滚动到页面顶部/底部。Twitter (X) 首页点击“顶”按钮滚动顶部、显示圆环动画并通过模拟主页按钮点击和验证

目前为 2025-04-20 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Twitter (X) & YouTube Refresh with Scroll Top/Bottom
  3. // @name:zh-CN Twitter & YouTube v1
  4. // @namespace https://gist.github.com/4lrick/bedb39b069be0e4c94dc20214137c9f5
  5. // @version 2.58
  6. // @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
  7. // @description:zh-CN 添加圆形按钮,显示“顶”和“底”文字,居中于渐变背景,滚动到页面顶部/底部。Twitter (X) 首页点击“顶”按钮滚动顶部、显示圆环动画并通过模拟主页按钮点击和验证
  8. // @author jiang
  9. // @match https://x.com/*
  10. // @match https://www.youtube.com/*
  11. // @match https://m.youtube.com/*
  12. // @icon https://www.google.com/s2/favicons?sz=64&domain=twitter.com
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_registerMenuCommand
  16. // @grant GM_unregisterMenuCommand
  17. // @license GPL-3.0-only
  18. // ==/UserScript==
  19.  
  20. (function() {
  21. 'use strict';
  22.  
  23. // Prevent duplicate script execution
  24. const key = encodeURIComponent('RefreshAndScroll:执行判断');
  25. if (window[key]) { return; }
  26. window[key] = true;
  27.  
  28. try {
  29. // Twitter (X) refresh interval configuration
  30. let refreshInterval = GM_getValue('refreshInterval', 5);
  31. let menuCommandId = null;
  32.  
  33. // Display a loading spinner animation
  34. function showSpinner() {
  35. const spinner = document.createElement('div');
  36. spinner.id = 'refresh-spinner';
  37. spinner.innerHTML = `
  38. <div class="neon-ring"></div>
  39. <div class="neon-ring inner-ring" style="animation-delay: 0.1s;"></div>
  40. <div class="spark"></div>
  41. <div class="spark" style="animation-delay: 0.2s; transform: rotate(90deg);"></div>
  42. <div class="spark" style="animation-delay: 0.4s; transform: rotate(180deg);"></div>
  43. <div class="spark" style="animation-delay: 0.6s; transform: rotate(270deg);"></div>
  44. `;
  45. spinner.style.cssText = `
  46. position: fixed;
  47. top: 50%;
  48. left: 50%;
  49. transform: translate(-50%, -50%);
  50. width: 100px;
  51. height: 100px;
  52. display: flex;
  53. justify-content: center;
  54. align-items: center;
  55. z-index: 10000;
  56. `;
  57. document.body.appendChild(spinner);
  58. return spinner;
  59. }
  60.  
  61. // Remove the spinner animation
  62. function hideSpinner(spinner) {
  63. if (spinner) spinner.remove();
  64. }
  65.  
  66. // Refresh Twitter (X) timeline
  67. function refreshTimeline() {
  68. if (window.location.href.startsWith('https://x.com/home')) {
  69. // Expanded selectors for Twitter home button
  70. const refreshButton = document.querySelector(
  71. '[href="/home"], [aria-label*="Home"], [data-testid="AppTabBar_Home_Link"], ' +
  72. '[role="link"][href="/home"], [aria-label*="Timeline"], [data-testid*="home"], ' +
  73. '[aria-label="Home timeline"], a[href="/home"]'
  74. );
  75. if (refreshButton) {
  76. const spinner = showSpinner();
  77. window.scrollTo({ top: 0, behavior: 'smooth' });
  78.  
  79. // Store initial top tweet ID to verify refresh
  80. const initialTopTweet = document.querySelector('article[data-testid="tweet"]');
  81. const initialTweetId = initialTopTweet ? initialTopTweet.querySelector('a[href*="/status/"]')?.href : null;
  82.  
  83. // Simulate click to refresh timeline
  84. refreshButton.click();
  85.  
  86. // Verify content update
  87. let attempts = 0;
  88. const maxAttempts = 5;
  89. const checkInterval = setInterval(() => {
  90. const newTopTweet = document.querySelector('article[data-testid="tweet"]');
  91. const newTweetId = newTopTweet ? newTopTweet.querySelector('a[href*="/status/"]')?.href : null;
  92.  
  93. if (newTweetId && newTweetId !== initialTweetId || attempts >= maxAttempts) {
  94. clearInterval(checkInterval);
  95. hideSpinner(spinner);
  96. } else {
  97. // Retry click if no update
  98. refreshButton.click();
  99. attempts++;
  100. }
  101. }, 1000);
  102. } else {
  103. console.log('RefreshAndScroll: Refresh button not found');
  104. const spinner = showSpinner();
  105. window.scrollTo({ top: 0, behavior: 'smooth' });
  106. setTimeout(() => hideSpinner(spinner), 1500);
  107. }
  108. }
  109. }
  110.  
  111. // Refresh non-Twitter pages (including YouTube)
  112. function refreshPage() {
  113. const spinner = showSpinner();
  114. window.scrollTo({ top: 0, behavior: 'smooth' });
  115. const reloadPromise = new Promise((resolve) => {
  116. window.addEventListener('load', resolve, { once: true });
  117. window.location.reload();
  118. });
  119. reloadPromise.then(() => {
  120. setTimeout(() => hideSpinner(spinner), 100);
  121. });
  122. }
  123.  
  124. // YouTube preloading and layout logic
  125. function customizeYouTubeLayout() {
  126. if (!window.location.href.startsWith('https://www.youtube.com/') && !window.location.href.startsWith('https://m.youtube.com/')) return;
  127.  
  128. // Preloading logic
  129. const preloadThreshold = window.innerHeight * 2; // Two screen heights
  130. let isLoading = false;
  131.  
  132. const scrollHandler = () => {
  133. if (isLoading) return;
  134.  
  135. const scrollPosition = window.scrollY + window.innerHeight;
  136. const pageHeight = document.documentElement.scrollHeight;
  137.  
  138. if (pageHeight - scrollPosition < preloadThreshold) {
  139. isLoading = true;
  140. const lastVideo = document.querySelector('ytd-rich-item-renderer:last-of-type:not([is-shorts])') ||
  141. document.querySelector('ytd-video-renderer:last-of-type:not([is-shorts])') ||
  142. document.querySelector('ytd-grid-video-renderer:last-of-type:not([is-shorts])');
  143. if (lastVideo) {
  144. lastVideo.scrollIntoView({ behavior: 'instant' });
  145. window.dispatchEvent(new Event('scroll'));
  146. setTimeout(() => window.dispatchEvent(new Event('scroll')), 100);
  147. setTimeout(() => window.dispatchEvent(new Event('scroll')), 200);
  148. const observer = new MutationObserver(() => {
  149. isLoading = false;
  150. observer.disconnect();
  151. });
  152. const target = document.querySelector('#contents') || document.body;
  153. observer.observe(target, { childList: true, subtree: true });
  154. setTimeout(() => {
  155. if (isLoading) {
  156. isLoading = false;
  157. observer.disconnect();
  158. }
  159. }, 5000);
  160. } else {
  161. isLoading = false;
  162. }
  163. }
  164. };
  165.  
  166. let isThrottled = false;
  167. window.addEventListener('scroll', () => {
  168. if (!isThrottled) {
  169. isThrottled = true;
  170. scrollHandler();
  171. setTimeout(() => { isThrottled = false; }, 200);
  172. }
  173. });
  174.  
  175. // Video layout and Shorts removal logic
  176. const style = document.createElement('style');
  177. style.textContent = `
  178. #contents.ytd-rich-grid-renderer {
  179. display: grid !important;
  180. grid-template-columns: repeat(auto-fill, minmax(50%, 1fr)) !important;
  181. gap: 10px !important;
  182. padding: 10px !important;
  183. box-sizing: border-box !important;
  184. }
  185. ytd-rich-item-renderer:not([is-shorts]), ytd-video-renderer:not([is-shorts]), ytd-grid-video-renderer:not([is-shorts]) {
  186. height: ${window.innerHeight / 4}px !important;
  187. margin: 0 !important;
  188. overflow: hidden !important;
  189. border-radius: 8px !important;
  190. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
  191. }
  192. 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) {
  193. grid-column: span 2 !important;
  194. width: 100% !important;
  195. }
  196. ytd-rich-item-renderer:not([is-shorts]) #thumbnail, ytd-video-renderer:not([is-shorts]) #thumbnail, ytd-grid-video-renderer:not([is-shorts]) #thumbnail {
  197. width: 100% !important;
  198. height: 100% !important;
  199. object-fit: cover !important;
  200. }
  201. ytd-rich-item-renderer:not([is-shorts]) #details, ytd-video-renderer:not([is-shorts]) #details, ytd-grid-video-renderer:not([is-shorts]) #details {
  202. display: none !important;
  203. }
  204. /* Shorts removal */
  205. ytd-reel-shelf-renderer,
  206. ytd-rich-shelf-renderer[is-shorts],
  207. ytd-shorts,
  208. ytd-reel-item-renderer,
  209. ytd-rich-item-renderer[is-shorts],
  210. ytd-video-renderer[is-shorts],
  211. ytd-grid-video-renderer[is-shorts],
  212. ytd-guide-entry-renderer:has(a[href*="/shorts"]),
  213. ytd-mini-guide-entry-renderer:has(a[href*="/shorts"]),
  214. ytm-pivot-bar-item-renderer:has(.pivot-shorts),
  215. ytm-pivot-bar-item-renderer[tab-identifier="FEshorts"],
  216. ytd-guide-entry-renderer:has([title="Shorts"]),
  217. ytd-mini-guide-entry-renderer:has([title="Shorts"]),
  218. ytm-rich-item-renderer:has([data-style="SHORTS"]),
  219. ytm-reel-shelf-renderer,
  220. ytm-rich-section-renderer:has(ytm-reel-shelf-renderer),
  221. #chips-wrapper yt-chip-cloud-chip-renderer[chip-style*="STYLE_HOME_FILTER"]:has(a[href*="/shorts"]),
  222. ytd-rich-section-renderer:has(#rich-shelf-header:contains("Shorts")),
  223. ytd-item-section-renderer:has([overlay-style="SHORTS"]),
  224. ytd-browse[page-subtype="shorts"],
  225. ytd-rich-grid-row:empty,
  226. #contents.ytd-rich-grid-row:empty {
  227. display: none !important;
  228. }
  229. /* Fix grid layout after removing Shorts */
  230. ytd-rich-grid-row, #contents.ytd-rich-grid-row {
  231. display: contents !important;
  232. }
  233. `;
  234. document.head.appendChild(style);
  235.  
  236. // Apply layout and remove Shorts dynamically
  237. const applyLayoutAndRemoveShorts = () => {
  238. // Apply video layout
  239. const contents = document.querySelector('#contents.ytd-rich-grid-renderer');
  240. if (contents) {
  241. contents.style.display = 'grid';
  242. const videos = document.querySelectorAll('ytd-rich-item-renderer:not([is-shorts]), ytd-video-renderer:not([is-shorts]), ytd-grid-video-renderer:not([is-shorts])');
  243. videos.forEach((video, index) => {
  244. video.style.height = `${window.innerHeight / 4}px`;
  245. if ((index + 1) % 3 === 0) {
  246. video.style.gridColumn = 'span 2';
  247. video.style.width = '100%';
  248. } else {
  249. video.style.gridColumn = 'auto';
  250. video.style.width = 'auto';
  251. }
  252. });
  253. }
  254.  
  255. // Remove Shorts elements
  256. const shortsSelectors = [
  257. 'ytd-reel-shelf-renderer',
  258. 'ytd-rich-shelf-renderer[is-shorts]',
  259. 'ytd-shorts',
  260. 'ytd-reel-item-renderer',
  261. 'ytd-rich-item-renderer[is-shorts]',
  262. 'ytd-video-renderer[is-shorts]',
  263. 'ytd-grid-video-renderer[is-shorts]',
  264. 'ytd-guide-entry-renderer:has(a[href*="/shorts"])',
  265. 'ytd-mini-guide-entry-renderer:has(a[href*="/shorts"])',
  266. 'ytm-pivot-bar-item-renderer:has(.pivot-shorts)',
  267. 'ytm-pivot-bar-item-renderer[tab-identifier="FEshorts"]',
  268. 'ytd-guide-entry-renderer:has([title="Shorts"])',
  269. 'ytd-mini-guide-entry-renderer:has([title="Shorts"])',
  270. 'ytm-rich-item-renderer:has([data-style="SHORTS"])',
  271. 'ytm-reel-shelf-renderer',
  272. 'ytm-rich-section-renderer:has(ytm-reel-shelf-renderer)',
  273. '#chips-wrapper yt-chip-cloud-chip-renderer[chip-style*="STYLE_HOME_FILTER"]:has(a[href*="/shorts"])',
  274. 'ytd-rich-section-renderer:has(#rich-shelf-header:contains("Shorts"))',
  275. 'ytd-item-section-renderer:has([overlay-style="SHORTS"])',
  276. 'ytd-browse[page-subtype="shorts"]'
  277. ];
  278. const shortsElements = document.querySelectorAll(shortsSelectors.join(','));
  279. shortsElements.forEach(el => el.remove());
  280.  
  281. // Clean up empty grid rows
  282. const emptyRows = document.querySelectorAll('ytd-rich-grid-row:empty, #contents.ytd-rich-grid-row:empty');
  283. emptyRows.forEach(row => row.remove());
  284. };
  285.  
  286. // Observe DOM changes to reapply layout and Shorts removal
  287. const observer = new MutationObserver(() => {
  288. applyLayoutAndRemoveShorts();
  289. });
  290. const target = document.body;
  291. observer.observe(target, { childList: true, subtree: true });
  292.  
  293. // Redirect /shorts/ URLs to /watch?v=
  294. const redirectShorts = () => {
  295. if (window.location.href.includes('youtube.com/shorts/')) {
  296. const newUrl = window.location.href.replace('/shorts/', '/watch?v=');
  297. window.location.replace(newUrl);
  298. }
  299. };
  300. redirectShorts();
  301. window.addEventListener('popstate', redirectShorts);
  302.  
  303. // Initial application
  304. applyLayoutAndRemoveShorts();
  305. }
  306.  
  307. // Set custom refresh interval for Twitter (X)
  308. function setCustomInterval() {
  309. const newInterval = prompt("Enter refresh interval in seconds:", refreshInterval);
  310. if (newInterval !== null) {
  311. const parsedInterval = parseInt(newInterval);
  312. if (!isNaN(parsedInterval) && parsedInterval > 0) {
  313. refreshInterval = parsedInterval;
  314. GM_setValue('refreshInterval', refreshInterval);
  315. updateMenuCommand();
  316. } else {
  317. alert("Please enter a valid positive number.");
  318. }
  319. }
  320. }
  321.  
  322. // Update the menu command for refresh interval
  323. function updateMenuCommand() {
  324. if (menuCommandId) {
  325. GM_unregisterMenuCommand(menuCommandId);
  326. }
  327. menuCommandId = GM_registerMenuCommand(`Set Refresh Interval (current: ${refreshInterval}s)`, setCustomInterval);
  328. }
  329.  
  330. // Scroll to top or bottom with conditional refresh
  331. function scrollToPosition(y, isTopButton = false) {
  332. window.scrollTo({ top: y, behavior: 'smooth' });
  333. if (y === 0) {
  334. if (window.location.href.startsWith('https://x.com/home')) {
  335. setTimeout(() => refreshTimeline(), 500);
  336. } else if (isTopButton) {
  337. setTimeout(() => refreshPage(), 500);
  338. }
  339. }
  340. }
  341.  
  342. // Create a visual click effect
  343. function createClickEffect(x, y) {
  344. const effect = document.createElement('div');
  345. effect.style.cssText = `
  346. position: fixed;
  347. left: ${x}px;
  348. top: ${y}px;
  349. width: 10px;
  350. height: 10px;
  351. background: transparent;
  352. border: 2px solid #00ff88;
  353. border-radius: 50%;
  354. pointer-events: none;
  355. z-index: 10000;
  356. animation: shockwave 0.5s ease-out forwards;
  357. box-shadow: 0 0 10px #00ccff, 0 0 20px #00ff88;
  358. `;
  359. document.body.appendChild(effect);
  360. setTimeout(() => effect.remove(), 500);
  361. }
  362.  
  363. // Inject CSS styles for buttons and animations
  364. const style = document.createElement('style');
  365. style.textContent = `
  366. @keyframes pulse {
  367. 0% { transform: scale(1); }
  368. 50% { transform: scale(1.1); }
  369. 100% { transform: scale(1); }
  370. }
  371. @keyframes shockwave {
  372. 0% {
  373. transform: scale(1);
  374. opacity: 1;
  375. border-width: 2px;
  376. }
  377. 100% {
  378. transform: scale(10);
  379. opacity: 0;
  380. border-width: 0;
  381. }
  382. }
  383. @keyframes neonPulse {
  384. 0% {
  385. transform: scale(1) rotate(0deg);
  386. opacity: 1;
  387. box-shadow: 0 0 10px #00ff88, 0 0 20px #00ccff;
  388. }
  389. 50% {
  390. transform: scale(1.2) rotate(180deg);
  391. opacity: 0.8;
  392. box-shadow: 0 0 20px #00ff88, 0 0 40px #00ccff;
  393. }
  394. 100% {
  395. transform: scale(1) rotate(360deg);
  396. opacity: 1;
  397. box-shadow: 0 0 10px #00ff88, 0 0 20px #00ccff;
  398. }
  399. }
  400. @keyframes sparkBurst {
  401. 0% {
  402. transform: translate(0, 0) scale(1);
  403. opacity: 1;
  404. }
  405. 100% {
  406. transform: translate(20px, 20px) scale(0);
  407. opacity: 0;
  408. }
  409. }
  410. .neon-ring {
  411. position: absolute;
  412. width: 80px;
  413. height: 80px;
  414. border: 4px solid transparent;
  415. border-top-color: #00ff88;
  416. border-right-color: #00ccff;
  417. border-radius: 50%;
  418. animation: neonPulse 1.5s linear infinite;
  419. }
  420. .inner-ring {
  421. width: 60px;
  422. height: 60px;
  423. border-top-color: #00ccff;
  424. border-right-color: #00ff88;
  425. animation-direction: reverse;
  426. }
  427. .spark {
  428. position: absolute;
  429. width: 8px;
  430. height: 8px;
  431. background: #ffffff;
  432. border-radius: 50%;
  433. box-shadow: 0 0 10px #00ff88, 0 0 15px #00ccff;
  434. animation: sparkBurst 1.5s ease-out infinite;
  435. }
  436. #sky-scrolltop, #sky-scrolltbtm {
  437. font-family: 'Microsoft YaHei', 'Arial', sans-serif !important;
  438. font-style: normal;
  439. font-weight: 700;
  440. font-size: 16px;
  441. line-height: 48px !important;
  442. text-align: center !important;
  443. background: linear-gradient(135deg, #00ff88, #00ccff) !important;
  444. border-radius: 50% !important;
  445. width: 48px !important;
  446. height: 48px !important;
  447. color: #ffffff !important;
  448. cursor: pointer;
  449. position: fixed;
  450. z-index: 999999;
  451. user-select: none;
  452. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  453. transition: transform 0.2s ease, box-shadow 0.2s ease;
  454. animation: pulse 2s infinite;
  455. visibility: visible !important;
  456. display: flex !important;
  457. justify-content: center !important;
  458. align-items: center !important;
  459. }
  460. #sky-scrolltop:hover, #sky-scrolltbtm:hover {
  461. transform: scale(1.15);
  462. box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);
  463. }
  464. #sky-scrolltop:active, #sky-scrolltbtm:active {
  465. transform: scale(0.95);
  466. }
  467. #sky-scrolltop.editing, #sky-scrolltbtm.editing {
  468. background: linear-gradient(135deg, #ff4444, #ff8888) !important;
  469. animation: none;
  470. }
  471. `;
  472. document.head.appendChild(style);
  473.  
  474. // Create scroll buttons for top and bottom navigation
  475. const scrollTop = document.createElement('div');
  476. scrollTop.id = 'sky-scrolltop';
  477. scrollTop.innerText = '顶';
  478. scrollTop.setAttribute('data-text', '顶');
  479. scrollTop.style.visibility = 'visible';
  480. document.body.appendChild(scrollTop);
  481. console.log('RefreshAndScroll: Created top button with text:', scrollTop.innerText);
  482.  
  483. const scrollBottom = document.createElement('div');
  484. scrollBottom.id = 'sky-scrolltbtm';
  485. scrollBottom.innerText = '底';
  486. scrollBottom.setAttribute('data-text', '底');
  487. scrollBottom.style.visibility = 'visible';
  488. document.body.appendChild(scrollBottom);
  489. console.log('RefreshAndScroll: Created bottom button with text:', scrollBottom.innerText);
  490.  
  491. // Periodically check and restore button text
  492. setInterval(() => {
  493. const topButton = document.querySelector('#sky-scrolltop');
  494. const bottomButton = document.querySelector('#sky-scrolltbtm');
  495. if (topButton && topButton.innerText !== '顶') {
  496. console.error('RefreshAndScroll: Top button text missing, restoring...');
  497. topButton.innerText = topButton.getAttribute('data-text') || '顶';
  498. }
  499. if (bottomButton && bottomButton.innerText !== '底') {
  500. console.error('RefreshAndScroll: Bottom button text missing, restoring...');
  501. bottomButton.innerText = bottomButton.getAttribute('data-text') || '底';
  502. }
  503. }, 1000);
  504.  
  505. // Initialize button positions
  506. let positions = GM_getValue('buttonPositions', { left: '20px', topBottom: '20%', bottomBottom: '12%' });
  507. scrollTop.style.left = positions.left;
  508. scrollTop.style.bottom = positions.topBottom;
  509. scrollBottom.style.left = positions.left;
  510. scrollBottom.style.bottom = positions.bottomBottom;
  511.  
  512. // Position editing logic
  513. let isEditing = false;
  514. let startX, startY, initialLeft, initialBottomTop;
  515. const fixedSpacing = 60;
  516.  
  517. // Start editing button positions on long press
  518. function startEditing(e) {
  519. e.preventDefault();
  520. isEditing = true;
  521. scrollTop.classList.add('editing');
  522. scrollBottom.classList.add('editing');
  523. startX = e.clientX || (e.touches && e.touches[0].clientX);
  524. startY = e.clientY || (e.touches && e.touches[0].clientY);
  525. initialLeft = parseFloat(scrollTop.style.left) || 20;
  526. initialBottomTop = parseFloat(scrollTop.style.bottom) || (window.innerHeight * 0.20);
  527. document.addEventListener('contextmenu', preventDefault, { capture: true });
  528. document.addEventListener('touchstart', preventDefault, { capture: true, passive: false });
  529. }
  530.  
  531. // Move buttons during editing mode
  532. function moveButtons(e) {
  533. if (!isEditing) return;
  534. e.preventDefault();
  535. const clientX = e.clientX || (e.touches && e.touches[0].clientX);
  536. const clientY = e.clientY || (e.touches && e.touches[0].clientY);
  537. if (!clientX || !clientY) return;
  538.  
  539. const deltaX = clientX - startX;
  540. const deltaY = startY - clientY;
  541.  
  542. const buttonWidth = 48;
  543. const buttonHeight = 48;
  544. let newLeft = initialLeft + deltaX;
  545. let newBottomTop = initialBottomTop + deltaY;
  546. let newBottomBtm = newBottomTop - fixedSpacing;
  547.  
  548. newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - buttonWidth));
  549. newBottomTop = Math.max(fixedSpacing, Math.min(newBottomTop, window.innerHeight - buttonHeight));
  550. newBottomBtm = newBottomTop - fixedSpacing;
  551.  
  552. if (newBottomBtm >= 0) {
  553. scrollTop.style.left = `${newLeft}px`;
  554. scrollTop.style.bottom = `${newBottomTop}px`;
  555. scrollBottom.style.left = `${newLeft}px`;
  556. scrollBottom.style.bottom = `${newBottomBtm}px`;
  557. }
  558. }
  559.  
  560. // Stop editing and save positions
  561. function stopEditing(e) {
  562. if (!isEditing) return;
  563. e.preventDefault();
  564. isEditing = false;
  565. scrollTop.classList.remove('editing');
  566. scrollBottom.classList.remove('editing');
  567. positions = {
  568. left: scrollTop.style.left,
  569. topBottom: scrollTop.style.bottom,
  570. bottomBottom: scrollBottom.style.bottom
  571. };
  572. GM_setValue('buttonPositions', positions);
  573. document.removeEventListener('contextmenu', preventDefault, { capture: true });
  574. document.removeEventListener('touchstart', preventDefault, { capture: true });
  575. }
  576.  
  577. // Prevent default browser actions during editing
  578. function preventDefault(e) {
  579. e.preventDefault();
  580. e.stopPropagation();
  581. }
  582.  
  583. // Long-press detection for editing mode
  584. let longPressTimer;
  585. const longPressDuration = 300;
  586.  
  587. function handleLongPressStart(e) {
  588. clearTimeout(longPressTimer);
  589. longPressTimer = setTimeout(() => startEditing(e), longPressDuration);
  590. }
  591.  
  592. function handleLongPressCancel() {
  593. clearTimeout(longPressTimer);
  594. }
  595.  
  596. // Attach event listeners to buttons
  597. scrollTop.addEventListener('mousedown', handleLongPressStart);
  598. scrollTop.addEventListener('touchstart', handleLongPressStart, { passive: false });
  599. scrollTop.addEventListener('mouseup', handleLongPressCancel);
  600. scrollTop.addEventListener('mouseleave', handleLongPressCancel);
  601. scrollTop.addEventListener('touchend', handleLongPressCancel);
  602. scrollTop.addEventListener('click', (e) => {
  603. if (!isEditing) {
  604. const rect = scrollTop.getBoundingClientRect();
  605. createClickEffect(rect.left + rect.width / 2, rect.top + rect.height / 2);
  606. scrollToPosition(0, true);
  607. }
  608. });
  609.  
  610. scrollBottom.addEventListener('mousedown', handleLongPressStart);
  611. scrollBottom.addEventListener('touchstart', handleLongPressStart, { passive: false });
  612. scrollBottom.addEventListener('mouseup', handleLongPressCancel);
  613. scrollBottom.addEventListener('mouseleave', handleLongPressCancel);
  614. scrollBottom.addEventListener('touchend', handleLongPressCancel);
  615. scrollBottom.addEventListener('click', (e) => {
  616. if (!isEditing) {
  617. const rect = scrollBottom.getBoundingClientRect();
  618. createClickEffect(rect.left + rect.width / 2, rect.top + rect.height / 2);
  619. scrollToPosition(document.body.scrollHeight);
  620. }
  621. });
  622.  
  623. // Global event listeners for button movement
  624. document.addEventListener('mousemove', moveButtons);
  625. document.addEventListener('touchmove', moveButtons, { passive: false });
  626. document.addEventListener('mouseup', stopEditing);
  627. document.addEventListener('touchend', stopEditing);
  628.  
  629. // Initialize Twitter (X) menu command
  630. if (window.location.href.startsWith('https://x.com/')) {
  631. updateMenuCommand();
  632. }
  633.  
  634. // Initialize YouTube customizations
  635. customizeYouTubeLayout();
  636. } catch (err) {
  637. console.log('RefreshAndScroll:', err);
  638. }
  639. })();