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-05 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name RSS: FreshRSS Gestures and Auto-Scroll
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.8
  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. const interactiveElements = ['A', 'BUTTON', 'INPUT', 'TEXTAREA', 'SELECT', 'LABEL'];
  79. if (interactiveElements.includes(event.target.tagName)) {
  80. debugLog('Ignored double-tap on interactive element');
  81. return;
  82. }
  83. const activeElement = event.target.closest('.flux.active');
  84. if (activeElement) {
  85. closeActiveArticle(activeElement);
  86. }
  87. });
  88.  
  89. // Touch event handlers for swipe detection
  90. document.addEventListener('touchstart', function(event) {
  91. touchStartX = event.touches[0].clientX;
  92. touchStartY = event.touches[0].clientY;
  93.  
  94. // If touch starts from near either edge, prevent default
  95. if (touchStartX <= EDGE_THRESHOLD ||
  96. touchStartX >= window.innerWidth - EDGE_THRESHOLD) {
  97. event.preventDefault();
  98. debugLog('Touch started near edge');
  99. }
  100. }, { passive: false });
  101.  
  102. document.addEventListener('touchmove', function(event) {
  103. const currentX = event.touches[0].clientX;
  104. const deltaX = currentX - touchStartX;
  105.  
  106. // Prevent default during edge swipes
  107. if ((touchStartX <= EDGE_THRESHOLD && deltaX > 0) ||
  108. (touchStartX >= window.innerWidth - EDGE_THRESHOLD && deltaX < 0)) {
  109. event.preventDefault();
  110. debugLog('Preventing default during edge swipe');
  111. }
  112. }, { passive: false });
  113.  
  114. document.addEventListener('touchend', function(event) {
  115. if (!touchStartX) return;
  116.  
  117. const touchEndX = event.changedTouches[0].clientX;
  118. const deltaX = touchEndX - touchStartX;
  119.  
  120. const activeElement = document.querySelector('.flux.active');
  121.  
  122. if (activeElement) {
  123. // Left-to-right swipe from left edge
  124. if (touchStartX <= EDGE_THRESHOLD && deltaX >= SWIPE_THRESHOLD) {
  125. event.preventDefault();
  126. scrollToNextElement(activeElement);
  127. debugLog('Left edge swipe detected');
  128. }
  129. // Right-to-left swipe from right edge
  130. else if (touchStartX >= window.innerWidth - EDGE_THRESHOLD &&
  131. deltaX <= -SWIPE_THRESHOLD) {
  132. event.preventDefault();
  133. scrollToNextElement(activeElement);
  134. debugLog('Right edge swipe detected');
  135. }
  136. }
  137.  
  138. // Reset touch tracking
  139. touchStartX = 0;
  140. touchStartY = 0;
  141. }, { passive: false });
  142.  
  143. let lastSpacePressTime = 0;
  144. const doublePressThreshold = 300; // Time in milliseconds (adjust as needed)
  145. document.addEventListener('keydown', function(event) {
  146. // Check if the pressed key is ' ' (space) and not in an input field or similar
  147. if (event.key === ' ' && !isInputField(event.target)) {
  148. const currentTime = Date.now();
  149.  
  150. // Check if the time between two spacebar presses is within the threshold
  151. if (currentTime - lastSpacePressTime <= doublePressThreshold) {
  152. event.preventDefault(); // Prevent the default spacebar behavior
  153. const activeElement = document.querySelector('.flux.active');
  154. if (activeElement) {
  155. scrollToNextElement(activeElement);
  156. debugLog('Double spacebar shortcut triggered scroll to next element');
  157. }
  158. }
  159.  
  160. // Update the last spacebar press time
  161. lastSpacePressTime = currentTime;
  162. }
  163. });
  164.  
  165. // Add keyboard shortcut key to scroll to next element with peek
  166. document.addEventListener('keydown', function(event) {
  167. // Check if the pressed key is 'v' and not in an input field or similar
  168. if (event.key === 'b' && !isInputField(event.target)) {
  169. const activeElement = document.querySelector('.flux.active');
  170. if (activeElement) {
  171. event.preventDefault();
  172. scrollToNextElement(activeElement);
  173. debugLog('Keyboard shortcut "v" triggered scroll to next element');
  174. }
  175. }
  176. });
  177.  
  178. // Function to check if the target element is an input field or similar
  179. function isInputField(element) {
  180. const inputTypes = ['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'];
  181. return inputTypes.includes(element.tagName) || element.isContentEditable;
  182. }
  183.  
  184. // Mutation observer to catch programmatic changes
  185. const observer = new MutationObserver((mutations) => {
  186. mutations.forEach((mutation) => {
  187. if (mutation.target.classList && mutation.target.classList.contains('flux')) {
  188. if (mutation.target.classList.contains('active')) {
  189. debugLog('Article became active via mutation');
  190. scrollToElement(mutation.target);
  191. }
  192. }
  193. });
  194. });
  195.  
  196. // Start observing the document with the configured parameters
  197. observer.observe(document.body, {
  198. attributes: true,
  199. attributeFilter: ['class'],
  200. subtree: true
  201. });
  202. })();