RSS: FreshRSS Gestures and Auto-Scroll

Gesture controls (swipe/double-tap) for FreshRSS: double-tap to close articles, edge swipe to jump to article end, and auto-scroll

当前为 2025-03-06 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name RSS: FreshRSS Gestures and Auto-Scroll
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.9
  5. // @description Gesture controls (swipe/double-tap) for FreshRSS: double-tap to close articles, edge swipe to jump to article end, and auto-scroll
  6. // @author Your Name
  7. // @homepage https://greasyfork.org/en/scripts/525912
  8. // @match http://192.168.1.2:1030/*
  9. // @grant none
  10. // ==/UserScript==
  11. (function() {
  12. 'use strict';
  13.  
  14. // Debug mode
  15. const DEBUG = false;
  16.  
  17. // Swipe detection configuration
  18. const EDGE_THRESHOLD = 10; // Distance from edge to start swipe
  19. const SWIPE_THRESHOLD = 50; // Minimum distance for a swipe
  20.  
  21. let touchStartX = 0;
  22. let touchStartY = 0;
  23.  
  24. function debugLog(message) {
  25. if (DEBUG) {
  26. console.log(`[FreshRSS Script]: ${message}`);
  27. }
  28. }
  29.  
  30. debugLog('Script loaded');
  31.  
  32. // Function to scroll to element
  33. function scrollToElement(element) {
  34. if (element) {
  35. const header = document.querySelector('header');
  36. const headerHeight = header ? header.offsetHeight : 0;
  37. const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
  38. const scrollTarget = elementPosition - headerHeight - 20; // 20px padding from header
  39.  
  40. window.scrollTo({
  41. top: scrollTarget,
  42. behavior: 'smooth'
  43. });
  44. debugLog('Scrolling element near top: ' + element.id);
  45. }
  46. }
  47.  
  48. // Function to scroll to next element with peek
  49. function scrollToNextElement(element) {
  50. if (element) {
  51. const nextElement = element.nextElementSibling;
  52. if (nextElement) {
  53. const header = document.querySelector('header');
  54. const headerHeight = header ? header.offsetHeight : 0;
  55. const nextElementPosition = nextElement.getBoundingClientRect().top + window.pageYOffset;
  56. const scrollTarget = nextElementPosition - headerHeight - 200; // px padding from header
  57.  
  58. window.scrollTo({
  59. top: scrollTarget,
  60. behavior: 'smooth'
  61. });
  62. debugLog('Scrolled to show next element near top');
  63. }
  64. }
  65. }
  66.  
  67. // Function to close active article
  68. function closeActiveArticle(element) {
  69. if (element) {
  70. element.classList.remove('active');
  71. debugLog('Closed article');
  72. element.scrollIntoView({ behavior: 'smooth', block: 'center' });
  73. }
  74. }
  75.  
  76. // Handle double-tap to close
  77. document.addEventListener('dblclick', function(event) {
  78. // Check if the device is a mobile device
  79. const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
  80. const isSmallScreen = window.innerWidth <= 768; // Adjust the screen width threshold as needed
  81. if (!isMobile || !isSmallScreen) {
  82. return; // Exit if not on a mobile device or if the screen is too large
  83. }
  84.  
  85. const interactiveElements = ['A', 'BUTTON', 'INPUT', 'TEXTAREA', 'SELECT', 'LABEL'];
  86. if (interactiveElements.includes(event.target.tagName)) {
  87. debugLog('Ignored double-tap on interactive element');
  88. return;
  89. }
  90. const activeElement = event.target.closest('.flux.active');
  91. if (activeElement) {
  92. closeActiveArticle(activeElement);
  93. }
  94. });
  95.  
  96. // Touch event handlers for swipe detection
  97. document.addEventListener('touchstart', function(event) {
  98. touchStartX = event.touches[0].clientX;
  99. touchStartY = event.touches[0].clientY;
  100.  
  101. // If touch starts from near either edge, prevent default
  102. if (touchStartX <= EDGE_THRESHOLD ||
  103. touchStartX >= window.innerWidth - EDGE_THRESHOLD) {
  104. event.preventDefault();
  105. debugLog('Touch started near edge');
  106. }
  107. }, { passive: false });
  108.  
  109. document.addEventListener('touchmove', function(event) {
  110. const currentX = event.touches[0].clientX;
  111. const deltaX = currentX - touchStartX;
  112.  
  113. // Prevent default during edge swipes
  114. if ((touchStartX <= EDGE_THRESHOLD && deltaX > 0) ||
  115. (touchStartX >= window.innerWidth - EDGE_THRESHOLD && deltaX < 0)) {
  116. event.preventDefault();
  117. debugLog('Preventing default during edge swipe');
  118. }
  119. }, { passive: false });
  120.  
  121. document.addEventListener('touchend', function(event) {
  122. if (!touchStartX) return;
  123.  
  124. const touchEndX = event.changedTouches[0].clientX;
  125. const deltaX = touchEndX - touchStartX;
  126.  
  127. const activeElement = document.querySelector('.flux.active');
  128.  
  129. if (activeElement) {
  130. // Left-to-right swipe from left edge
  131. if (touchStartX <= EDGE_THRESHOLD && deltaX >= SWIPE_THRESHOLD) {
  132. event.preventDefault();
  133. scrollToNextElement(activeElement);
  134. debugLog('Left edge swipe detected');
  135. }
  136. // Right-to-left swipe from right edge
  137. else if (touchStartX >= window.innerWidth - EDGE_THRESHOLD &&
  138. deltaX <= -SWIPE_THRESHOLD) {
  139. event.preventDefault();
  140. scrollToNextElement(activeElement);
  141. debugLog('Right edge swipe detected');
  142. }
  143. }
  144.  
  145. // Reset touch tracking
  146. touchStartX = 0;
  147. touchStartY = 0;
  148. }, { passive: false });
  149.  
  150. let lastSpacePressTime = 0;
  151. const doublePressThreshold = 300; // Time in milliseconds (adjust as needed)
  152. document.addEventListener('keydown', function(event) {
  153. // Check if the pressed key is ' ' (space) and not in an input field or similar
  154. if (event.key === ' ' && !isInputField(event.target)) {
  155. const currentTime = Date.now();
  156.  
  157. // Check if the time between two spacebar presses is within the threshold
  158. if (currentTime - lastSpacePressTime <= doublePressThreshold) {
  159. event.preventDefault(); // Prevent the default spacebar behavior
  160. const activeElement = document.querySelector('.flux.active');
  161. if (activeElement) {
  162. scrollToNextElement(activeElement);
  163. debugLog('Double spacebar shortcut triggered scroll to next element');
  164. }
  165. }
  166.  
  167. // Update the last spacebar press time
  168. lastSpacePressTime = currentTime;
  169. }
  170. });
  171.  
  172. // Add keyboard shortcut key to scroll to next element with peek
  173. document.addEventListener('keydown', function(event) {
  174. // Check if the pressed key is 'v' and not in an input field or similar
  175. if (event.key === 'b' && !isInputField(event.target)) {
  176. const activeElement = document.querySelector('.flux.active');
  177. if (activeElement) {
  178. event.preventDefault();
  179. scrollToNextElement(activeElement);
  180. debugLog('Keyboard shortcut "v" triggered scroll to next element');
  181. }
  182. }
  183. });
  184.  
  185. // Function to check if the target element is an input field or similar
  186. function isInputField(element) {
  187. const inputTypes = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'];
  188. return inputTypes.includes(element.tagName) || element.isContentEditable;
  189. }
  190.  
  191. // Mutation observer to catch programmatic changes
  192. const observer = new MutationObserver((mutations) => {
  193. mutations.forEach((mutation) => {
  194. if (mutation.target.classList && mutation.target.classList.contains('flux')) {
  195. if (mutation.target.classList.contains('active')) {
  196. debugLog('Article became active via mutation');
  197. scrollToElement(mutation.target);
  198. }
  199. }
  200. });
  201. });
  202.  
  203. // Start observing the document with the configured parameters
  204. observer.observe(document.body, {
  205. attributes: true,
  206. attributeFilter: ['class'],
  207. subtree: true
  208. });
  209. })();