Mayx's Home Page https://mabbs.github.io
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

235 lines
6.8 KiB

  1. /**
  2. * RSS/Atom Feed Preview for Links Table
  3. */
  4. (function() {
  5. const existingPreviews = document.querySelectorAll('#rss-feed-preview');
  6. existingPreviews.forEach(el => el.remove());
  7. const CORS_PROXY = 'https://cors-anywhere.mayx.eu.org/?';
  8. const createPreviewElement = () => {
  9. const existingPreview = document.getElementById('rss-feed-preview');
  10. if (existingPreview) {
  11. return existingPreview;
  12. }
  13. const previewEl = document.createElement('div');
  14. previewEl.id = 'rss-feed-preview';
  15. previewEl.style.cssText = `
  16. position: fixed;
  17. display: none;
  18. width: 300px;
  19. max-height: 400px;
  20. overflow-y: auto;
  21. background-color: white;
  22. border: 1px solid #ccc;
  23. border-radius: 5px;
  24. padding: 10px;
  25. box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  26. z-index: 1000;
  27. font-size: 14px;
  28. line-height: 1.4;
  29. `;
  30. document.body.appendChild(previewEl);
  31. return previewEl;
  32. };
  33. const parseRSS = (xmlText) => {
  34. const parser = new DOMParser();
  35. const xml = parser.parseFromString(xmlText, 'text/xml');
  36. const rssItems = xml.querySelectorAll('item');
  37. if (rssItems.length > 0) {
  38. return Array.from(rssItems).slice(0, 5).map(item => {
  39. return {
  40. title: item.querySelector('title')?.textContent || 'No title',
  41. date: item.querySelector('pubDate')?.textContent || 'No date',
  42. };
  43. });
  44. }
  45. const atomItems = xml.querySelectorAll('entry');
  46. if (atomItems.length > 0) {
  47. return Array.from(atomItems).slice(0, 5).map(item => {
  48. return {
  49. title: item.querySelector('title')?.textContent || 'No title',
  50. date: item.querySelector('updated')?.textContent || 'No date',
  51. };
  52. });
  53. }
  54. return null;
  55. };
  56. const checkFeed = async (url) => {
  57. try {
  58. const response = await fetch(CORS_PROXY + url);
  59. if (!response.ok) {
  60. return null;
  61. }
  62. const text = await response.text();
  63. return parseRSS(text);
  64. } catch (error) {
  65. return null;
  66. }
  67. };
  68. const findFeedUrl = async (siteUrl, linkElement) => {
  69. if (linkElement && linkElement.hasAttribute('data-feed')) {
  70. const dataFeedUrl = linkElement.getAttribute('data-feed');
  71. if (dataFeedUrl) {
  72. const feedItems = await checkFeed(dataFeedUrl);
  73. if (feedItems) {
  74. return { url: dataFeedUrl, items: feedItems };
  75. }
  76. }
  77. }
  78. return null;
  79. };
  80. const escapeHTML = (str) => {
  81. return String(str).replace(/[&<>"'/]/g, (c) => ({
  82. '&': '&amp;',
  83. '<': '&lt;',
  84. '>': '&gt;',
  85. '"': '&quot;',
  86. "'": '&#39;',
  87. '/': '&#x2F;'
  88. }[c]));
  89. };
  90. const renderFeedItems = (previewEl, items, siteName) => {
  91. if (!items || items.length === 0) {
  92. previewEl.innerHTML = '<p>No feed items found.</p>';
  93. return;
  94. }
  95. let html = `<h3>Latest from ${siteName}</h3><ul style="list-style: none; padding: 0; margin: 0;">`;
  96. items.forEach(item => {
  97. const safeTitle = escapeHTML(item.title);
  98. const safeDate = escapeHTML(new Date(item.date).toLocaleDateString());
  99. html += `
  100. <li style="margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee;">
  101. <div style="color: #24292e; font-weight: bold;">
  102. ${safeTitle}
  103. </div>
  104. <div style="color: #586069; font-size: 12px; margin: 3px 0;">
  105. ${safeDate}
  106. </div>
  107. </li>
  108. `;
  109. });
  110. html += '</ul>';
  111. previewEl.innerHTML = html;
  112. };
  113. const positionPreview = (previewEl, event) => {
  114. const viewportWidth = window.innerWidth;
  115. const viewportHeight = window.innerHeight;
  116. let left = event.clientX + 20;
  117. let top = event.clientY + 20;
  118. const rect = previewEl.getBoundingClientRect();
  119. if (left + rect.width > viewportWidth) {
  120. left = event.clientX - rect.width - 20;
  121. }
  122. if (top + rect.height > viewportHeight) {
  123. top = event.clientY - rect.height - 20;
  124. }
  125. left = Math.max(10, left);
  126. top = Math.max(10, top);
  127. previewEl.style.left = `${left}px`;
  128. previewEl.style.top = `${top}px`;
  129. };
  130. const initFeedPreview = () => {
  131. const previewEl = createPreviewElement();
  132. const tableLinks = document.querySelectorAll('main table tbody tr td a');
  133. const feedCache = {};
  134. let currentLink = null;
  135. let loadingTimeout = null;
  136. tableLinks.forEach(link => {
  137. link.addEventListener('mouseenter', async (event) => {
  138. currentLink = link;
  139. const url = link.getAttribute('href');
  140. const siteName = link.textContent;
  141. previewEl.innerHTML = '<p>Checking for RSS/Atom feed...</p>';
  142. previewEl.style.display = 'block';
  143. positionPreview(previewEl, event);
  144. if (loadingTimeout) {
  145. clearTimeout(loadingTimeout);
  146. }
  147. loadingTimeout = setTimeout(async () => {
  148. if (feedCache[url]) {
  149. renderFeedItems(previewEl, feedCache[url].items, siteName);
  150. positionPreview(previewEl, event); // Reposition after content is loaded
  151. return;
  152. }
  153. const feedData = await findFeedUrl(url, link);
  154. if (currentLink === link) {
  155. if (feedData) {
  156. feedCache[url] = feedData;
  157. renderFeedItems(previewEl, feedData.items, siteName);
  158. positionPreview(previewEl, event); // Reposition after content is loaded
  159. } else {
  160. previewEl.style.display = 'none';
  161. }
  162. }
  163. }, 300);
  164. });
  165. link.addEventListener('mousemove', (event) => {
  166. if (previewEl.style.display === 'block') {
  167. window.requestAnimationFrame(() => {
  168. positionPreview(previewEl, event);
  169. });
  170. }
  171. });
  172. link.addEventListener('mouseleave', () => {
  173. if (loadingTimeout) {
  174. clearTimeout(loadingTimeout);
  175. loadingTimeout = null;
  176. }
  177. currentLink = null;
  178. previewEl.style.display = 'none';
  179. });
  180. });
  181. document.addEventListener('click', (event) => {
  182. if (!previewEl.contains(event.target)) {
  183. previewEl.style.display = 'none';
  184. }
  185. });
  186. };
  187. if (!window.rssFeedPreviewInitialized) {
  188. window.rssFeedPreviewInitialized = true;
  189. if (document.readyState === 'loading') {
  190. document.addEventListener('DOMContentLoaded', initFeedPreview);
  191. } else {
  192. initFeedPreview();
  193. }
  194. }
  195. })();

Powered by TurnKey Linux.