ijohn07 commited on
Commit
6c7859a
Β·
verified Β·
1 Parent(s): daa4d83

Upload 6 files

Browse files
Files changed (4) hide show
  1. index.html +11 -5
  2. scripts/app.js +85 -7
  3. scripts/sidebar.js +30 -7
  4. styles/main.css +124 -2
index.html CHANGED
@@ -60,7 +60,8 @@
60
  </nav>
61
  <div class="header-actions">
62
  <button class="btn-icon" id="btn-theme" title="Toggle theme">πŸŒ™</button>
63
- <button class="btn-icon" id="btn-refresh" title="Refresh feeds">πŸ”„</button>
 
64
  <button class="btn-icon" id="btn-settings" title="Settings">βš™οΈ</button>
65
  <button class="btn-icon" id="btn-about" title="About">πŸ’š</button>
66
  </div>
@@ -106,7 +107,7 @@
106
  <!-- Bookmarks -->
107
  <section class="sidebar-section">
108
  <h3 class="sidebar-title collapsible" data-target="bookmarks-content">
109
- πŸ“Œ Bookmarks
110
  <span class="section-toggle">β–Ό</span>
111
  </h3>
112
  <div class="section-content" id="bookmarks-content">
@@ -161,6 +162,9 @@
161
  <!-- Sidebar Toggle (Mobile) -->
162
  <button class="sidebar-toggle" id="sidebar-toggle" title="Toggle Sidebar">πŸ“‹</button>
163
 
 
 
 
164
  <!-- Footer -->
165
  <footer class="footer">
166
  <p>
@@ -182,6 +186,8 @@
182
  <kbd>S</kbd> Sidebar
183
  <span class="divider">β€’</span>
184
  <kbd>T</kbd> Theme
 
 
185
  </div>
186
 
187
  <!-- About Modal -->
@@ -287,10 +293,10 @@
287
  <input type="checkbox" id="opt-show-descriptions" checked />
288
  Show article descriptions
289
  </label>
290
- <label class="checkbox-label">
 
291
  <input type="number" id="opt-max-items" value="10" min="5" max="100" />
292
- Max items per source
293
- </label>
294
  </section>
295
  </div>
296
  </div>
 
60
  </nav>
61
  <div class="header-actions">
62
  <button class="btn-icon" id="btn-theme" title="Toggle theme">πŸŒ™</button>
63
+ <button class="btn-icon" id="btn-refresh"
64
+ title="Refresh feeds (Shift+Click = force refresh)">πŸ”„</button>
65
  <button class="btn-icon" id="btn-settings" title="Settings">βš™οΈ</button>
66
  <button class="btn-icon" id="btn-about" title="About">πŸ’š</button>
67
  </div>
 
107
  <!-- Bookmarks -->
108
  <section class="sidebar-section">
109
  <h3 class="sidebar-title collapsible" data-target="bookmarks-content">
110
+ πŸ“Œ Bookmarks <span class="bookmark-count" id="bookmark-count"></span>
111
  <span class="section-toggle">β–Ό</span>
112
  </h3>
113
  <div class="section-content" id="bookmarks-content">
 
162
  <!-- Sidebar Toggle (Mobile) -->
163
  <button class="sidebar-toggle" id="sidebar-toggle" title="Toggle Sidebar">πŸ“‹</button>
164
 
165
+ <!-- Back to Top Button -->
166
+ <button class="back-to-top" id="back-to-top" title="Back to top" aria-label="Scroll back to top">↑</button>
167
+
168
  <!-- Footer -->
169
  <footer class="footer">
170
  <p>
 
186
  <kbd>S</kbd> Sidebar
187
  <span class="divider">β€’</span>
188
  <kbd>T</kbd> Theme
189
+ <span class="divider">β€’</span>
190
+ <kbd>↑</kbd> Top
191
  </div>
192
 
193
  <!-- About Modal -->
 
293
  <input type="checkbox" id="opt-show-descriptions" checked />
294
  Show article descriptions
295
  </label>
296
+ <div class="number-input-row">
297
+ <label for="opt-max-items">Max items per source</label>
298
  <input type="number" id="opt-max-items" value="10" min="5" max="100" />
299
+ </div>
 
300
  </section>
301
  </div>
302
  </div>
scripts/app.js CHANGED
@@ -269,10 +269,12 @@ class RSSHubApp {
269
  return;
270
  }
271
 
272
- // Escape to close modals
273
  if (e.key === "Escape") {
274
  this.elements.modalAbout.classList.remove("active");
275
  this.elements.modalSettings.classList.remove("active");
 
 
276
  }
277
 
278
  // R to refresh (Shift+R for force refresh)
@@ -311,8 +313,17 @@ class RSSHubApp {
311
  if (e.key === "t" && !e.ctrlKey && !e.metaKey) {
312
  this.toggleTheme();
313
  }
 
 
 
 
 
 
314
  });
315
 
 
 
 
316
  // Category navigation
317
  this.elements.navButtons.forEach(btn => {
318
  btn.addEventListener("click", () => {
@@ -416,6 +427,51 @@ class RSSHubApp {
416
  );
417
  }
418
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
  /**
420
  * Set active language filter
421
  */
@@ -898,30 +954,53 @@ class RSSHubApp {
898
  }
899
 
900
  /**
901
- * Add custom feed
902
  */
903
- addCustomFeed() {
904
  const nameInput = document.getElementById("custom-feed-name");
905
  const urlInput = document.getElementById("custom-feed-url");
906
  const categorySelect = document.getElementById("custom-feed-category");
 
907
 
908
  const name = nameInput.value.trim();
909
  const url = urlInput.value.trim();
910
  const category = categorySelect.value;
911
 
912
  if (!name || !url) {
913
- alert("Please enter both a name and URL for the feed.");
914
  return;
915
  }
916
 
917
- // Validate URL
918
  try {
919
  new URL(url);
920
  } catch {
921
- alert("Please enter a valid URL.");
922
  return;
923
  }
924
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
925
  // Generate unique ID
926
  const id = "custom_" + Date.now();
927
 
@@ -1088,4 +1167,3 @@ document.addEventListener("DOMContentLoaded", () => {
1088
  app = new RSSHubApp();
1089
  window.app = app; // Expose for event handlers
1090
  });
1091
-
 
269
  return;
270
  }
271
 
272
+ // Escape to close modals and sidebar
273
  if (e.key === "Escape") {
274
  this.elements.modalAbout.classList.remove("active");
275
  this.elements.modalSettings.classList.remove("active");
276
+ // Also close mobile sidebar
277
+ this.elements.sidebar?.classList.remove("open");
278
  }
279
 
280
  // R to refresh (Shift+R for force refresh)
 
313
  if (e.key === "t" && !e.ctrlKey && !e.metaKey) {
314
  this.toggleTheme();
315
  }
316
+
317
+ // Home or ArrowUp (when not in input) to scroll to top
318
+ if (e.key === "Home" || (e.key === "ArrowUp" && e.ctrlKey)) {
319
+ e.preventDefault();
320
+ this.scrollToTop();
321
+ }
322
  });
323
 
324
+ // Back to top button
325
+ this.setupBackToTop();
326
+
327
  // Category navigation
328
  this.elements.navButtons.forEach(btn => {
329
  btn.addEventListener("click", () => {
 
427
  );
428
  }
429
 
430
+ /**
431
+ * Setup Back to Top button behavior
432
+ */
433
+ setupBackToTop() {
434
+ const backToTopBtn = document.getElementById("back-to-top");
435
+ if (!backToTopBtn) return;
436
+
437
+ // Show/hide button based on scroll position
438
+ let ticking = false;
439
+ window.addEventListener(
440
+ "scroll",
441
+ () => {
442
+ if (!ticking) {
443
+ requestAnimationFrame(() => {
444
+ const scrollY = window.scrollY || document.documentElement.scrollTop;
445
+ // Show button after scrolling 400px
446
+ if (scrollY > 400) {
447
+ backToTopBtn.classList.add("visible");
448
+ } else {
449
+ backToTopBtn.classList.remove("visible");
450
+ }
451
+ ticking = false;
452
+ });
453
+ ticking = true;
454
+ }
455
+ },
456
+ { passive: true }
457
+ );
458
+
459
+ // Click handler
460
+ backToTopBtn.addEventListener("click", () => {
461
+ this.scrollToTop();
462
+ });
463
+ }
464
+
465
+ /**
466
+ * Smooth scroll to top of page
467
+ */
468
+ scrollToTop() {
469
+ window.scrollTo({
470
+ top: 0,
471
+ behavior: "smooth"
472
+ });
473
+ }
474
+
475
  /**
476
  * Set active language filter
477
  */
 
954
  }
955
 
956
  /**
957
+ * Add custom feed with validation
958
  */
959
+ async addCustomFeed() {
960
  const nameInput = document.getElementById("custom-feed-name");
961
  const urlInput = document.getElementById("custom-feed-url");
962
  const categorySelect = document.getElementById("custom-feed-category");
963
+ const addBtn = document.getElementById("btn-add-feed");
964
 
965
  const name = nameInput.value.trim();
966
  const url = urlInput.value.trim();
967
  const category = categorySelect.value;
968
 
969
  if (!name || !url) {
970
+ this.showToast("Please enter both name and URL", "error");
971
  return;
972
  }
973
 
974
+ // Validate URL format
975
  try {
976
  new URL(url);
977
  } catch {
978
+ this.showToast("Please enter a valid URL", "error");
979
  return;
980
  }
981
 
982
+ // Show loading state
983
+ const originalText = addBtn.textContent;
984
+ addBtn.textContent = "Testing...";
985
+ addBtn.disabled = true;
986
+
987
+ // Test if the URL is actually a valid RSS feed
988
+ try {
989
+ const result = await this.parser.fetchFeed({ url, name, id: "test" });
990
+ if (result.status !== "success") {
991
+ throw new Error(result.error || "Invalid RSS feed");
992
+ }
993
+ } catch (e) {
994
+ addBtn.textContent = originalText;
995
+ addBtn.disabled = false;
996
+ this.showToast(`Invalid feed: ${e.message}`, "error");
997
+ return;
998
+ }
999
+
1000
+ // Restore button
1001
+ addBtn.textContent = originalText;
1002
+ addBtn.disabled = false;
1003
+
1004
  // Generate unique ID
1005
  const id = "custom_" + Date.now();
1006
 
 
1167
  app = new RSSHubApp();
1168
  window.app = app; // Expose for event handlers
1169
  });
 
scripts/sidebar.js CHANGED
@@ -86,12 +86,26 @@ class SidebarManager {
86
  this.handleSearch("");
87
  });
88
 
89
- // Clear bookmarks
90
- this.elements.btnClearBookmarks?.addEventListener("click", () => {
91
- if (confirm("Clear all bookmarks?")) {
 
92
  this.bookmarks = [];
93
  this.saveBookmarks();
94
  this.renderBookmarks();
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  }
96
  });
97
  }
@@ -146,7 +160,8 @@ class SidebarManager {
146
  }
147
 
148
  // Show loading indicator immediately for better UX
149
- this.elements.searchResults.innerHTML = '<span class="search-hint">πŸ” Searching...</span>';
 
150
 
151
  const queryLower = query.toLowerCase();
152
  const results = this.allArticles
@@ -179,7 +194,8 @@ class SidebarManager {
179
 
180
  highlightMatch(text, query) {
181
  const escaped = this.escapeHtml(text);
182
- const regex = new RegExp(`(${this.escapeRegex(query)})`, "gi");
 
183
  return escaped.replace(regex, "<mark>$1</mark>");
184
  }
185
 
@@ -237,6 +253,12 @@ class SidebarManager {
237
  }
238
 
239
  renderBookmarks() {
 
 
 
 
 
 
240
  if (this.bookmarks.length === 0) {
241
  this.elements.bookmarksList.innerHTML =
242
  '<span class="empty-hint">No bookmarks yet. Click ⭐ on articles to save them.</span>';
@@ -443,9 +465,10 @@ class SidebarManager {
443
  const isHot = count >= maxCount * 0.7;
444
  return `
445
  <button class="trending-tag ${isHot ? "hot" : ""}"
446
- onclick="sidebar.filterByTag('${this.escapeHtml(word)}')">
 
447
  ${word}
448
- <span class="tag-count">${count}</span>
449
  </button>
450
  `;
451
  })
 
86
  this.handleSearch("");
87
  });
88
 
89
+ // Clear bookmarks with double-click protection
90
+ this.elements.btnClearBookmarks?.addEventListener("click", e => {
91
+ // Use data attribute for confirmation state
92
+ if (e.target.dataset.confirmClear === "true") {
93
  this.bookmarks = [];
94
  this.saveBookmarks();
95
  this.renderBookmarks();
96
+ e.target.textContent = "Clear All";
97
+ e.target.dataset.confirmClear = "false";
98
+ // Refresh main feed to update bookmark icons
99
+ if (this.app) this.app.renderFeeds();
100
+ this.app?.showToast("All bookmarks cleared", "info");
101
+ } else {
102
+ e.target.textContent = "Click again to confirm";
103
+ e.target.dataset.confirmClear = "true";
104
+ // Reset after 3 seconds
105
+ setTimeout(() => {
106
+ e.target.textContent = "Clear All";
107
+ e.target.dataset.confirmClear = "false";
108
+ }, 3000);
109
  }
110
  });
111
  }
 
160
  }
161
 
162
  // Show loading indicator immediately for better UX
163
+ this.elements.searchResults.innerHTML =
164
+ '<span class="search-hint"><span class="search-spinner"></span> Searching...</span>';
165
 
166
  const queryLower = query.toLowerCase();
167
  const results = this.allArticles
 
194
 
195
  highlightMatch(text, query) {
196
  const escaped = this.escapeHtml(text);
197
+ const escapedQuery = this.escapeHtml(query);
198
+ const regex = new RegExp(`(${this.escapeRegex(escapedQuery)})`, "gi");
199
  return escaped.replace(regex, "<mark>$1</mark>");
200
  }
201
 
 
253
  }
254
 
255
  renderBookmarks() {
256
+ // Update bookmark count badge
257
+ const countBadge = document.getElementById("bookmark-count");
258
+ if (countBadge) {
259
+ countBadge.textContent = this.bookmarks.length > 0 ? `(${this.bookmarks.length})` : "";
260
+ }
261
+
262
  if (this.bookmarks.length === 0) {
263
  this.elements.bookmarksList.innerHTML =
264
  '<span class="empty-hint">No bookmarks yet. Click ⭐ on articles to save them.</span>';
 
465
  const isHot = count >= maxCount * 0.7;
466
  return `
467
  <button class="trending-tag ${isHot ? "hot" : ""}"
468
+ onclick="sidebar.filterByTag('${this.escapeHtml(word)}')"
469
+ aria-label="Filter by ${this.escapeHtml(word)}, ${count} articles">
470
  ${word}
471
+ <span class="tag-count" aria-hidden="true">${count}</span>
472
  </button>
473
  `;
474
  })
styles/main.css CHANGED
@@ -79,6 +79,9 @@
79
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
80
  --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
81
 
 
 
 
82
  /* Theme indicator */
83
  --theme-icon: "β˜€οΈ";
84
  }
@@ -103,6 +106,9 @@
103
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
104
  --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
105
 
 
 
 
106
  --theme-icon: "β˜€οΈ";
107
  }
108
  }
@@ -435,7 +441,7 @@ a:hover {
435
 
436
  /* Subtle zebra striping for better visual distinction */
437
  .article-item-wrapper:nth-child(odd) {
438
- background: rgba(255, 255, 255, 0.015);
439
  }
440
 
441
  .article-item-wrapper:nth-child(even) {
@@ -814,6 +820,35 @@ a:hover {
814
  margin-right: 8px;
815
  }
816
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
817
  .hint {
818
  font-size: 0.8rem;
819
  color: var(--text-muted);
@@ -896,9 +931,17 @@ a:hover {
896
 
897
  .status-bar {
898
  flex-direction: column;
899
- gap: 4px;
900
  text-align: center;
901
  }
 
 
 
 
 
 
 
 
902
  }
903
 
904
  /* ============================================
@@ -954,6 +997,14 @@ a:hover {
954
  transition: transform var(--transition-fast);
955
  }
956
 
 
 
 
 
 
 
 
 
957
  .sidebar-title.collapsed .section-toggle {
958
  transform: rotate(-90deg);
959
  }
@@ -1069,6 +1120,19 @@ a:hover {
1069
  margin-top: 2px;
1070
  }
1071
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1072
  /* === Bookmarks === */
1073
  .bookmarks-list {
1074
  display: flex;
@@ -1379,6 +1443,64 @@ a:hover {
1379
  transform: scale(1.1);
1380
  }
1381
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1382
  /* === Article Bookmark Button === */
1383
  .article-bookmark {
1384
  width: 32px;
 
79
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
80
  --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
81
 
82
+ /* Zebra striping for light theme */
83
+ --zebra-odd: rgba(0, 0, 0, 0.02);
84
+
85
  /* Theme indicator */
86
  --theme-icon: "β˜€οΈ";
87
  }
 
106
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07);
107
  --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
108
 
109
+ /* Zebra striping for light theme */
110
+ --zebra-odd: rgba(0, 0, 0, 0.02);
111
+
112
  --theme-icon: "β˜€οΈ";
113
  }
114
  }
 
441
 
442
  /* Subtle zebra striping for better visual distinction */
443
  .article-item-wrapper:nth-child(odd) {
444
+ background: var(--zebra-odd, rgba(255, 255, 255, 0.015));
445
  }
446
 
447
  .article-item-wrapper:nth-child(even) {
 
820
  margin-right: 8px;
821
  }
822
 
823
+ /* Number input row (for max items) */
824
+ .number-input-row {
825
+ display: flex;
826
+ align-items: center;
827
+ justify-content: space-between;
828
+ gap: 12px;
829
+ padding: 8px 0;
830
+ color: var(--text-secondary);
831
+ }
832
+
833
+ .number-input-row label {
834
+ flex: 1;
835
+ }
836
+
837
+ .number-input-row input[type="number"] {
838
+ width: 70px;
839
+ padding: 6px 10px;
840
+ background: var(--bg-tertiary);
841
+ border: 1px solid var(--border-color);
842
+ border-radius: var(--radius-sm);
843
+ color: var(--text-primary);
844
+ text-align: center;
845
+ }
846
+
847
+ .number-input-row input[type="number"]:focus {
848
+ outline: none;
849
+ border-color: var(--ivy-green);
850
+ }
851
+
852
  .hint {
853
  font-size: 0.8rem;
854
  color: var(--text-muted);
 
931
 
932
  .status-bar {
933
  flex-direction: column;
934
+ gap: 8px;
935
  text-align: center;
936
  }
937
+
938
+ .status-bar .lang-filter {
939
+ order: -1;
940
+ }
941
+
942
+ .status-bar .status-text {
943
+ font-size: 0.8rem;
944
+ }
945
  }
946
 
947
  /* ============================================
 
997
  transition: transform var(--transition-fast);
998
  }
999
 
1000
+ /* Bookmark count badge */
1001
+ .bookmark-count {
1002
+ font-size: 0.75rem;
1003
+ color: var(--ivy-green);
1004
+ font-weight: 600;
1005
+ margin-left: 4px;
1006
+ }
1007
+
1008
  .sidebar-title.collapsed .section-toggle {
1009
  transform: rotate(-90deg);
1010
  }
 
1120
  margin-top: 2px;
1121
  }
1122
 
1123
+ /* Search spinner */
1124
+ .search-spinner {
1125
+ display: inline-block;
1126
+ width: 12px;
1127
+ height: 12px;
1128
+ border: 2px solid var(--border-color);
1129
+ border-top-color: var(--ivy-green);
1130
+ border-radius: 50%;
1131
+ animation: spin 0.8s linear infinite;
1132
+ margin-right: 6px;
1133
+ vertical-align: middle;
1134
+ }
1135
+
1136
  /* === Bookmarks === */
1137
  .bookmarks-list {
1138
  display: flex;
 
1443
  transform: scale(1.1);
1444
  }
1445
 
1446
+ /* === Back to Top Button === */
1447
+ .back-to-top {
1448
+ position: fixed;
1449
+ bottom: 20px;
1450
+ right: 20px;
1451
+ width: 44px;
1452
+ height: 44px;
1453
+ background: var(--bg-tertiary);
1454
+ border: 1px solid var(--border-color);
1455
+ border-radius: 50%;
1456
+ font-size: 1.2rem;
1457
+ color: var(--text-secondary);
1458
+ cursor: pointer;
1459
+ box-shadow: var(--shadow-md);
1460
+ z-index: 200;
1461
+ opacity: 0;
1462
+ visibility: hidden;
1463
+ transform: translateY(20px);
1464
+ transition: all var(--transition-normal);
1465
+ }
1466
+
1467
+ .back-to-top.visible {
1468
+ opacity: 1;
1469
+ visibility: visible;
1470
+ transform: translateY(0);
1471
+ }
1472
+
1473
+ .back-to-top:hover {
1474
+ background: var(--ivy-green);
1475
+ border-color: var(--ivy-green);
1476
+ color: #fff;
1477
+ transform: scale(1.1);
1478
+ }
1479
+
1480
+ .back-to-top:focus {
1481
+ outline: 2px solid var(--ivy-green);
1482
+ outline-offset: 2px;
1483
+ }
1484
+
1485
+ .back-to-top:focus:not(:focus-visible) {
1486
+ outline: none;
1487
+ }
1488
+
1489
+ /* Position adjustment when keyboard hint is visible */
1490
+ @media (min-width: 901px) {
1491
+ .back-to-top {
1492
+ bottom: 60px; /* Above keyboard hint */
1493
+ }
1494
+ }
1495
+
1496
+ /* Mobile/Tablet: position to not overlap sidebar toggle */
1497
+ @media (max-width: 1100px) {
1498
+ .back-to-top {
1499
+ right: 80px; /* Left of sidebar toggle */
1500
+ bottom: 20px;
1501
+ }
1502
+ }
1503
+
1504
  /* === Article Bookmark Button === */
1505
  .article-bookmark {
1506
  width: 32px;