🔖 Bookmarks
+ Zurück zum Dashboard +Über die Bookmarks kannst du auf einen Schlag mehrere relevante Suchanfragen öffnen.
+ + + +diff --git a/web/Dockerfile b/web/Dockerfile index d9d248c..a062301 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,8 +1,10 @@ FROM nginx:alpine COPY index.html /usr/share/nginx/html/ +COPY posts.html /usr/share/nginx/html/ COPY dashboard.html /usr/share/nginx/html/ COPY settings.html /usr/share/nginx/html/ +COPY bookmarks.html /usr/share/nginx/html/ COPY style.css /usr/share/nginx/html/ COPY dashboard.css /usr/share/nginx/html/ COPY settings.css /usr/share/nginx/html/ diff --git a/web/app.js b/web/app.js index 6a82c24..4119280 100644 --- a/web/app.js +++ b/web/app.js @@ -145,6 +145,9 @@ function scheduleUpdatesReconnect() { } function startUpdatesStream() { + if (isBookmarksPage) { + return; + } if (typeof EventSource === 'undefined') { console.warn('EventSource wird von diesem Browser nicht unterstützt. Fallback auf Polling.'); return; @@ -246,6 +249,11 @@ const bookmarkForm = document.getElementById('bookmarkForm'); const bookmarkNameInput = document.getElementById('bookmarkName'); const bookmarkQueryInput = document.getElementById('bookmarkQuery'); const bookmarkCancelBtn = document.getElementById('bookmarkCancelBtn'); +const bookmarkSearchInput = document.getElementById('bookmarkSearchInput'); +const bookmarkSortSelect = document.getElementById('bookmarkSortSelect'); +const bookmarkSortDirectionToggle = document.getElementById('bookmarkSortDirectionToggle'); +const profileSelectElement = document.getElementById('profileSelect'); +const isBookmarksPage = document.body.classList.contains('bookmarks-page'); const REFRESH_SETTINGS_KEY = 'trackerRefreshSettings'; const SORT_SETTINGS_KEY = 'trackerSortSettings'; @@ -296,6 +304,9 @@ let manualPostModalPreviousOverflow = ''; let activeDeadlinePicker = null; let bookmarkPanelVisible = false; let bookmarkOutsideHandler = null; +let bookmarkSearchTerm = ''; +let bookmarkSortMode = 'recent'; +let bookmarkSortDirection = 'desc'; const INITIAL_POST_LIMIT = 10; const POST_LOAD_INCREMENT = 10; @@ -501,6 +512,83 @@ function sortBookmarksByRecency(list) { }); } +function getBookmarkLabelForComparison(bookmark = {}) { + const label = typeof bookmark.label === 'string' ? bookmark.label.trim() : ''; + const query = typeof bookmark.query === 'string' ? bookmark.query.trim() : ''; + return label || query || ''; +} + +function getBookmarkClickTimestamp(bookmark = {}) { + if (bookmark.last_clicked_at) { + const ts = new Date(bookmark.last_clicked_at).getTime(); + if (!Number.isNaN(ts)) { + return ts; + } + } + return 0; +} + +function sortBookmarksForDisplay(list) { + const items = [...list]; + if (bookmarkSortMode === 'label') { + items.sort((a, b) => { + const labelA = getBookmarkLabelForComparison(a); + const labelB = getBookmarkLabelForComparison(b); + const result = labelA.localeCompare(labelB, 'de', { sensitivity: 'base' }); + return bookmarkSortDirection === 'desc' ? -result : result; + }); + return items; + } + + items.sort((a, b) => { + const diff = getBookmarkClickTimestamp(b) - getBookmarkClickTimestamp(a); + if (diff !== 0) { + return bookmarkSortDirection === 'desc' ? diff : -diff; + } + const fallback = getBookmarkLabelForComparison(a).localeCompare( + getBookmarkLabelForComparison(b), + 'de', + { sensitivity: 'base' } + ); + return bookmarkSortDirection === 'desc' ? fallback : -fallback; + }); + + return items; +} + +function filterBookmarksBySearch(list) { + if (!bookmarkSearchTerm) { + return [...list]; + } + const term = bookmarkSearchTerm.toLowerCase(); + return list.filter((bookmark) => { + const label = (bookmark.label || bookmark.query || '').toLowerCase(); + const query = (bookmark.query || '').toLowerCase(); + return label.includes(term) || query.includes(term); + }); +} + +function getRecentBookmarks(list) { + const recent = sortBookmarksByRecency(list); + const RECENT_LIMIT = 5; + return recent.filter((bookmark) => bookmark.last_clicked_at).slice(0, RECENT_LIMIT); +} + +function updateBookmarkSortDirectionUI() { + if (!bookmarkSortDirectionToggle) { + return; + } + const isAsc = bookmarkSortDirection === 'asc'; + bookmarkSortDirectionToggle.setAttribute('aria-pressed', isAsc ? 'true' : 'false'); + bookmarkSortDirectionToggle.setAttribute('title', isAsc ? 'Älteste zuerst' : 'Neueste zuerst'); + const icon = bookmarkSortDirectionToggle.querySelector('.bookmark-sort__direction-icon'); + if (icon) { + icon.textContent = isAsc ? '▲' : '▼'; + } else { + bookmarkSortDirectionToggle.textContent = isAsc ? '▲' : '▼'; + } +} + const DEFAULT_BOOKMARK_LAST_CLICK_KEY = 'trackerDefaultBookmarkLastClickedAt'; const bookmarkState = { @@ -954,18 +1042,9 @@ function renderBookmarks() { isDefault: true }; - const sorted = sortBookmarksByRecency(dynamicBookmarks); - const recent = []; - const RECENT_LIMIT = 5; - - sorted.forEach((bookmark) => { - if (bookmark.last_clicked_at && recent.length < RECENT_LIMIT) { - recent.push(bookmark); - } - }); - - const alphabeticalAll = [...dynamicBookmarks] - .sort((a, b) => a.label.localeCompare(b.label, 'de', { sensitivity: 'base' })); + const filteredBookmarks = filterBookmarksBySearch(dynamicBookmarks); + const sortedForAll = sortBookmarksForDisplay(filteredBookmarks); + const recent = bookmarkSearchTerm ? [] : getRecentBookmarks(filteredBookmarks); const sections = []; @@ -977,17 +1056,25 @@ function renderBookmarks() { }); } + const allItems = bookmarkSearchTerm ? sortedForAll : [staticDefault, ...sortedForAll]; + const allTitle = bookmarkSearchTerm + ? `Suchergebnisse${filteredBookmarks.length ? ` (${filteredBookmarks.length})` : ''}` + : 'Alle Bookmarks'; + sections.push({ - id: 'all', - title: 'Alle Bookmarks', - items: [staticDefault, ...alphabeticalAll] + id: bookmarkSearchTerm ? 'search' : 'all', + title: allTitle, + items: allItems }); + let renderedAnySection = false; + sections.forEach((section) => { if (!section.items.length) { return; } + renderedAnySection = true; const sectionElement = document.createElement('section'); sectionElement.className = 'bookmark-section'; sectionElement.dataset.section = section.id; @@ -1012,6 +1099,17 @@ function renderBookmarks() { sectionElement.appendChild(list); bookmarksList.appendChild(sectionElement); }); + + if (!renderedAnySection) { + const empty = document.createElement('div'); + empty.className = 'bookmark-empty'; + if (bookmarkSearchTerm) { + empty.textContent = `Keine Bookmarks gefunden für „${bookmarkSearchTerm}“.`; + } else { + empty.textContent = 'Noch keine Bookmarks gespeichert.'; + } + bookmarksList.appendChild(empty); + } } function resetBookmarkForm() { @@ -1197,6 +1295,34 @@ function initializeBookmarks() { if (bookmarkForm) { bookmarkForm.addEventListener('submit', handleBookmarkSubmit); } + + if (bookmarkSearchInput) { + bookmarkSearchInput.addEventListener('input', () => { + bookmarkSearchTerm = typeof bookmarkSearchInput.value === 'string' + ? bookmarkSearchInput.value.trim() + : ''; + renderBookmarks(); + }); + } + + if (bookmarkSortSelect) { + bookmarkSortSelect.addEventListener('change', () => { + const value = bookmarkSortSelect.value; + if (value === 'label' || value === 'recent') { + bookmarkSortMode = value; + renderBookmarks(); + } + }); + } + + if (bookmarkSortDirectionToggle) { + bookmarkSortDirectionToggle.addEventListener('click', () => { + bookmarkSortDirection = bookmarkSortDirection === 'desc' ? 'asc' : 'desc'; + updateBookmarkSortDirectionUI(); + renderBookmarks(); + }); + updateBookmarkSortDirectionUI(); + } } function getSortSettingsPageKey() { @@ -2288,6 +2414,10 @@ function applyAutoRefreshSettings() { : ''; } + if (isBookmarksPage) { + return; + } + if (!autoRefreshSettings.enabled) { return; } @@ -2582,7 +2712,9 @@ function applyProfileNumber(profileNumber, { fromBackend = false } = {}) { return; } - document.getElementById('profileSelect').value = String(profileNumber); + if (profileSelectElement) { + profileSelectElement.value = String(profileNumber); + } if (currentProfile === profileNumber) { if (!fromBackend) { @@ -2598,8 +2730,10 @@ function applyProfileNumber(profileNumber, { fromBackend = false } = {}) { pushProfileState(currentProfile); } - resetVisibleCount(); - renderPosts(); + if (!isBookmarksPage) { + resetVisibleCount(); + renderPosts(); + } } // Load profile from localStorage @@ -2637,9 +2771,11 @@ function startProfilePolling() { } // Profile selector change handler -document.getElementById('profileSelect').addEventListener('change', (e) => { - saveProfile(parseInt(e.target.value, 10)); -}); +if (profileSelectElement) { + profileSelectElement.addEventListener('change', (e) => { + saveProfile(parseInt(e.target.value, 10)); + }); +} // Tab switching document.querySelectorAll('.tab-btn').forEach(btn => { @@ -2673,6 +2809,9 @@ if (autoRefreshToggle) { autoRefreshSettings.enabled = !!autoRefreshToggle.checked; saveAutoRefreshSettings(); applyAutoRefreshSettings(); + if (isBookmarksPage) { + return; + } if (autoRefreshSettings.enabled && updatesStreamHealthy) { console.info('Live-Updates sind aktiv; automatisches Refresh bleibt pausiert.'); } @@ -3980,7 +4119,9 @@ function checkAutoCheck() { } catch (error) { console.warn('Konnte check-Parameter nicht entfernen:', error); } - fetchPosts({ showLoader: false }); + if (!isBookmarksPage) { + fetchPosts({ showLoader: false }); + } }).catch(console.error); } } @@ -4049,10 +4190,12 @@ loadAutoRefreshSettings(); initializeFocusParams(); initializeTabFromUrl(); loadSortMode(); -resetManualPostForm(); -loadProfile(); -startProfilePolling(); -fetchPosts(); -checkAutoCheck(); -startUpdatesStream(); +if (!isBookmarksPage) { + resetManualPostForm(); + loadProfile(); + startProfilePolling(); + fetchPosts(); + checkAutoCheck(); + startUpdatesStream(); +} applyAutoRefreshSettings(); diff --git a/web/bookmarks.html b/web/bookmarks.html new file mode 100644 index 0000000..69f1ccb --- /dev/null +++ b/web/bookmarks.html @@ -0,0 +1,62 @@ + + +
+ + +Über die Bookmarks kannst du auf einen Schlag mehrere relevante Suchanfragen öffnen.
+ + + +