RSS: FreshRSS Gestures and Auto-Scroll

Gesture controls (swipe/double-tap) for FreshRSS: double-tap top half to close articles; double-tap bottom half and edge swipe to jump to article end

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