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

  1. // ==UserScript==
  2. // @name RSS: FreshRSS Gestures and Auto-Scroll
  3. // @namespace http://tampermonkey.net/
  4. // @version 3.3
  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. // Debug mode
  14. const DEBUG = false;
  15. // Swipe detection configuration
  16. const EDGE_THRESHOLD = 50; // Distance from edge to start swipe
  17. const SWIPE_THRESHOLD = 50; // Minimum distance for a swipe
  18. let touchStartX = 0;
  19. let touchStartY = 0;
  20. function debugLog(message) {
  21. if (DEBUG) {
  22. console.log(`[FreshRSS Script]: ${message}`);
  23. }
  24. }
  25. debugLog('Script loaded');
  26.  
  27. // Function to scroll to element
  28. function scrollToElement(element) {
  29. if (element) {
  30. const header = document.querySelector('header');
  31. const headerHeight = header ? header.offsetHeight : 0;
  32. const elementPosition = element.getBoundingClientRect().top + window.pageYOffset;
  33. const scrollTarget = elementPosition - headerHeight - 20; // 20px padding from header
  34. window.scrollTo({
  35. top: scrollTarget,
  36. behavior: 'smooth'
  37. });
  38. debugLog('Scrolling element near top: ' + element.id);
  39. }
  40. }
  41.  
  42. // Function to scroll to next element with peek
  43. function scrollToNextElement(element) {
  44. if (element) {
  45. const nextElement = element.nextElementSibling;
  46. if (nextElement) {
  47. const header = document.querySelector('header');
  48. const headerHeight = header ? header.offsetHeight : 0;
  49. const nextElementPosition = nextElement.getBoundingClientRect().top + window.pageYOffset;
  50. const scrollTarget = nextElementPosition - headerHeight - 25; // px padding from header
  51. window.scrollTo({
  52. top: scrollTarget,
  53. behavior: 'smooth'
  54. });
  55. debugLog('Scrolled to show next element near top');
  56. }
  57. }
  58. }
  59.  
  60. // Function to close active article
  61. function closeActiveArticle(element) {
  62. if (element) {
  63. element.classList.remove('active');
  64. debugLog('Closed article');
  65. element.scrollIntoView({ behavior: 'smooth', block: 'center' });
  66. }
  67. }
  68.  
  69. // Handle double-tap to close
  70. document.addEventListener('dblclick', function(event) {
  71. const interactiveElements = ['A', 'BUTTON', 'INPUT', 'TEXTAREA', 'SELECT', 'LABEL'];
  72. if (interactiveElements.includes(event.target.tagName)) {
  73. debugLog('Ignored double-tap on interactive element');
  74. return;
  75. }
  76. const activeElement = event.target.closest('.flux.active');
  77. if (activeElement) {
  78. closeActiveArticle(activeElement);
  79. }
  80. });
  81.  
  82. // Touch event handlers for swipe detection
  83. document.addEventListener('touchstart', function(event) {
  84. touchStartX = event.touches[0].clientX;
  85. touchStartY = event.touches[0].clientY;
  86. // If touch starts from near either edge, prevent default
  87. if (touchStartX <= EDGE_THRESHOLD ||
  88. touchStartX >= window.innerWidth - EDGE_THRESHOLD) {
  89. event.preventDefault();
  90. debugLog('Touch started near edge');
  91. }
  92. }, { passive: false });
  93.  
  94. document.addEventListener('touchmove', function(event) {
  95. const currentX = event.touches[0].clientX;
  96. const deltaX = currentX - touchStartX;
  97. // Prevent default during edge swipes
  98. if ((touchStartX <= EDGE_THRESHOLD && deltaX > 0) ||
  99. (touchStartX >= window.innerWidth - EDGE_THRESHOLD && deltaX < 0)) {
  100. event.preventDefault();
  101. debugLog('Preventing default during edge swipe');
  102. }
  103. }, { passive: false });
  104.  
  105. document.addEventListener('touchend', function(event) {
  106. if (!touchStartX) return;
  107.  
  108. const touchEndX = event.changedTouches[0].clientX;
  109. const deltaX = touchEndX - touchStartX;
  110.  
  111. const activeElement = document.querySelector('.flux.active');
  112. if (activeElement) {
  113. // Left-to-right swipe from left edge
  114. if (touchStartX <= EDGE_THRESHOLD && deltaX >= SWIPE_THRESHOLD) {
  115. event.preventDefault();
  116. scrollToNextElement(activeElement);
  117. debugLog('Left edge swipe detected');
  118. }
  119. // Right-to-left swipe from right edge
  120. else if (touchStartX >= window.innerWidth - EDGE_THRESHOLD &&
  121. deltaX <= -SWIPE_THRESHOLD) {
  122. event.preventDefault();
  123. scrollToNextElement(activeElement);
  124. debugLog('Right edge swipe detected');
  125. }
  126. }
  127.  
  128. // Reset touch tracking
  129. touchStartX = 0;
  130. touchStartY = 0;
  131. }, { passive: false });
  132.  
  133. // Mutation observer to catch programmatic changes
  134. const observer = new MutationObserver((mutations) => {
  135. mutations.forEach((mutation) => {
  136. if (mutation.target.classList && mutation.target.classList.contains('flux')) {
  137. if (mutation.target.classList.contains('active')) {
  138. debugLog('Article became active via mutation');
  139. scrollToElement(mutation.target);
  140. }
  141. }
  142. });
  143. });
  144.  
  145. // Start observing the document with the configured parameters
  146. observer.observe(document.body, {
  147. attributes: true,
  148. attributeFilter: ['class'],
  149. subtree: true
  150. });
  151. })();