daily bookmarks

This commit is contained in:
2025-12-02 21:21:08 +01:00
parent 3ff25d3f7e
commit 839bd24309
10 changed files with 3303 additions and 49 deletions

View File

@@ -20,6 +20,9 @@ let currentTab = 'pending';
let posts = [];
let includeExpiredPosts = false;
let profilePollTimer = null;
const MERGE_MAX_SELECTION = 2;
let mergeMode = false;
const mergeSelection = new Set();
const UPDATES_RECONNECT_DELAY = 5000;
let updatesEventSource = null;
let updatesReconnectTimer = null;
@@ -232,11 +235,17 @@ const bookmarkForm = document.getElementById('bookmarkForm');
const bookmarkNameInput = document.getElementById('bookmarkName');
const bookmarkQueryInput = document.getElementById('bookmarkQuery');
const bookmarkCancelBtn = document.getElementById('bookmarkCancelBtn');
const bookmarkQuickForm = document.getElementById('bookmarkQuickForm');
const bookmarkQuickQueryInput = document.getElementById('bookmarkQuickQuery');
const bookmarkQuickStatus = document.getElementById('bookmarkQuickStatus');
const bookmarkSearchInput = document.getElementById('bookmarkSearchInput');
const bookmarkSortSelect = document.getElementById('bookmarkSortSelect');
const bookmarkSortDirectionToggle = document.getElementById('bookmarkSortDirectionToggle');
const profileSelectElement = document.getElementById('profileSelect');
const includeExpiredToggle = document.getElementById('includeExpiredToggle');
const mergeControls = document.getElementById('mergeControls');
const mergeModeToggle = document.getElementById('mergeModeToggle');
const mergeSubmitBtn = document.getElementById('mergeSubmitBtn');
const REFRESH_SETTINGS_KEY = 'trackerRefreshSettings';
const SORT_SETTINGS_KEY = 'trackerSortSettings';
@@ -294,6 +303,37 @@ function updateIncludeExpiredToggleVisibility() {
wrapper.style.display = currentTab === 'all' ? 'inline-flex' : 'none';
}
function resetMergeSelection() {
mergeSelection.clear();
}
function updateMergeControlsUI() {
if (!mergeControls) {
return;
}
const isAllTab = currentTab === 'all';
mergeControls.hidden = !isAllTab;
mergeControls.style.display = isAllTab ? 'flex' : 'none';
if (!isAllTab && mergeMode) {
mergeMode = false;
resetMergeSelection();
}
if (mergeModeToggle) {
mergeModeToggle.disabled = !isAllTab;
mergeModeToggle.classList.toggle('active', mergeMode);
mergeModeToggle.textContent = mergeMode ? 'Merge-Modus: aktiv' : 'Merge-Modus';
}
if (mergeSubmitBtn) {
const count = mergeSelection.size;
mergeSubmitBtn.disabled = !mergeMode || !isAllTab || count !== MERGE_MAX_SELECTION;
mergeSubmitBtn.textContent = `Beiträge mergen (${count}/${MERGE_MAX_SELECTION})`;
}
}
function initializeFocusParams() {
try {
const params = new URLSearchParams(window.location.search);
@@ -996,6 +1036,21 @@ function buildBookmarkSearchQueries(baseQuery) {
return BOOKMARK_SUFFIXES.map((suffix) => `${trimmed} ${suffix}`.trim());
}
function openBookmarkQueries(baseQuery) {
const queries = Array.isArray(baseQuery) ? baseQuery : buildBookmarkSearchQueries(baseQuery);
let opened = 0;
queries.forEach((searchTerm) => {
const url = buildBookmarkSearchUrl(searchTerm);
if (url) {
window.open(url, '_blank', 'noopener');
opened += 1;
}
});
return opened;
}
function openBookmark(bookmark) {
if (!bookmark) {
return;
@@ -1032,13 +1087,7 @@ function openBookmark(bookmark) {
markBookmarkClick(stateBookmark.id);
}
queries.forEach((searchTerm) => {
const url = buildBookmarkSearchUrl(searchTerm);
if (url) {
window.open(url, '_blank', 'noopener');
}
});
openBookmarkQueries(queries);
}
async function markBookmarkClick(bookmarkId) {
@@ -1380,6 +1429,45 @@ async function handleBookmarkSubmit(event) {
}
}
function setBookmarkQuickStatus(message, isError = false) {
if (!bookmarkQuickStatus) {
return;
}
const hasMessage = typeof message === 'string' && message.trim();
bookmarkQuickStatus.hidden = !hasMessage;
bookmarkQuickStatus.textContent = hasMessage ? message : '';
bookmarkQuickStatus.classList.toggle('bookmark-status--error', !!isError);
}
function handleBookmarkQuickSubmit(event) {
event.preventDefault();
if (!bookmarkQuickForm) {
return;
}
const query = bookmarkQuickQueryInput ? bookmarkQuickQueryInput.value.trim() : '';
if (!query) {
setBookmarkQuickStatus('Bitte gib einen Suchbegriff ein.', true);
if (bookmarkQuickQueryInput) {
bookmarkQuickQueryInput.focus();
}
return;
}
const opened = openBookmarkQueries(query);
if (opened > 0) {
setBookmarkQuickStatus(`Suche „${query}“ geöffnet (ohne Speicherung).`);
if (bookmarkQuickQueryInput) {
bookmarkQuickQueryInput.value = '';
bookmarkQuickQueryInput.focus();
}
} else {
setBookmarkQuickStatus('Konnte die Suche nicht öffnen.', true);
}
}
function initializeBookmarks() {
if (!bookmarksList) {
return;
@@ -1418,7 +1506,17 @@ function initializeBookmarks() {
bookmarkForm.addEventListener('submit', handleBookmarkSubmit);
}
if (bookmarkSearchInput) {
if (bookmarkQuickForm) {
bookmarkQuickForm.addEventListener('submit', handleBookmarkQuickSubmit);
}
if (bookmarkQuickQueryInput) {
bookmarkQuickQueryInput.addEventListener('input', () => {
setBookmarkQuickStatus('');
});
}
if (bookmarkSearchInput) {
bookmarkSearchInput.value = bookmarkSearchTerm;
bookmarkSearchInput.addEventListener('input', () => {
bookmarkSearchTerm = typeof bookmarkSearchInput.value === 'string'
@@ -1665,8 +1763,13 @@ function setTab(tab, { updateUrl = true } = {}) {
} else {
currentTab = 'pending';
}
if (currentTab !== 'all' && mergeMode) {
mergeMode = false;
resetMergeSelection();
}
updateTabButtons();
loadSortMode({ fromTabChange: true });
updateMergeControlsUI();
if (updateUrl) {
updateTabInUrl();
}
@@ -2982,6 +3085,27 @@ if (searchInput) {
});
}
if (mergeModeToggle) {
mergeModeToggle.addEventListener('click', () => {
if (currentTab !== 'all') {
alert('Merge-Modus ist nur in „Alle Beiträge“ verfügbar.');
return;
}
mergeMode = !mergeMode;
if (!mergeMode) {
resetMergeSelection();
}
updateMergeControlsUI();
renderPosts();
});
}
if (mergeSubmitBtn) {
mergeSubmitBtn.addEventListener('click', () => {
mergeSelectedPosts();
});
}
if (sortModeSelect) {
sortModeSelect.addEventListener('change', () => {
const value = sortModeSelect.value;
@@ -3195,6 +3319,104 @@ function highlightPostCard(post) {
clearFocusParamsFromUrl();
}
function toggleMergeSelection(postId, checkboxEl = null) {
if (!mergeMode || currentTab !== 'all') {
resetMergeSelection();
if (checkboxEl) {
checkboxEl.checked = false;
}
updateMergeControlsUI();
return;
}
if (mergeSelection.has(postId)) {
mergeSelection.delete(postId);
} else {
if (mergeSelection.size >= MERGE_MAX_SELECTION) {
alert(`Es können maximal ${MERGE_MAX_SELECTION} Beiträge ausgewählt werden.`);
if (checkboxEl) {
checkboxEl.checked = false;
}
updateMergeControlsUI();
return;
}
mergeSelection.add(postId);
}
updateMergeControlsUI();
}
async function mergeSelectedPosts() {
if (!mergeMode || currentTab !== 'all') {
alert('Mergen ist nur in „Alle Beiträge“ möglich.');
return;
}
if (mergeSelection.size !== MERGE_MAX_SELECTION) {
alert('Bitte genau zwei Beiträge auswählen.');
return;
}
const [primaryId, secondaryId] = Array.from(mergeSelection);
const confirmed = window.confirm(
`Beitrag ${primaryId} als Haupt-URL behalten und Beitrag ${secondaryId} anhängen?`
);
if (!confirmed) {
return;
}
if (mergeSubmitBtn) {
mergeSubmitBtn.disabled = true;
mergeSubmitBtn.textContent = 'Mergen...';
}
try {
const response = await apiFetch(`${API_URL}/posts/merge`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
primary_post_id: primaryId,
secondary_post_id: secondaryId
})
});
if (!response.ok) {
const data = await response.json().catch(() => null);
const message = data && data.error ? data.error : 'Beiträge konnten nicht gemerged werden.';
throw new Error(message);
}
const mergedPost = await response.json();
posts = posts
.filter((post) => {
if (post.id === secondaryId) {
return false;
}
if (post.id === primaryId) {
return false;
}
return true;
});
posts.push(mergedPost);
sortPostsByCreatedAt();
mergeMode = false;
resetMergeSelection();
updateMergeControlsUI();
renderPosts();
alert('Beiträge wurden gemerged.');
} catch (error) {
console.error('Merge error:', error);
alert(error.message || 'Beiträge konnten nicht gemerged werden.');
} finally {
if (mergeSubmitBtn) {
mergeSubmitBtn.disabled = false;
}
updateMergeControlsUI();
}
}
// Render posts
function renderPosts() {
hideLoading();
@@ -3206,6 +3428,7 @@ function renderPosts() {
}
updateIncludeExpiredToggleVisibility();
updateMergeControlsUI();
closeActiveDeadlinePicker();
updateTabButtons();
cleanupLoadMoreObserver();
@@ -3359,6 +3582,13 @@ function attachPostEventHandlers(post, status) {
return;
}
const mergeCheckbox = card.querySelector('.merge-checkbox');
if (mergeCheckbox) {
mergeCheckbox.addEventListener('change', () => {
toggleMergeSelection(post.id, mergeCheckbox);
});
}
const openBtn = card.querySelector('.btn-open');
if (openBtn) {
openBtn.addEventListener('click', () => openPost(post.id));
@@ -3548,6 +3778,15 @@ function createPostCard(post, status, meta = {}) {
const tabTotalCount = typeof meta.tabTotalCount === 'number' ? meta.tabTotalCount : posts.length;
const searchActive = !!meta.searchActive;
const indexBadge = displayIndex !== null ? `<span class="post-index">#${String(displayIndex).padStart(2, '0')}</span>` : '';
const mergeSelectable = mergeMode && currentTab === 'all';
const mergeCheckboxHtml = mergeSelectable
? `
<label class="merge-select">
<input type="checkbox" class="merge-checkbox" data-post-id="${post.id}" ${mergeSelection.has(post.id) ? 'checked' : ''}>
<span>Merge</span>
</label>
`
: '';
const profileRowsHtml = status.profileStatuses.map((profileStatus) => {
const classes = ['profile-line', `profile-line--${profileStatus.status}`];
@@ -3690,6 +3929,7 @@ function createPostCard(post, status, meta = {}) {
<div class="post-card ${status.isComplete ? 'complete' : ''}" id="post-${post.id}">
<div class="post-header">
<div class="post-title-with-checkbox">
${mergeCheckboxHtml}
${indexBadge}
<div class="post-title">${escapeHtml(titleText)}</div>
<label class="success-checkbox success-checkbox--header">
@@ -4308,6 +4548,7 @@ initializeBookmarks();
loadAutoRefreshSettings();
initializeFocusParams();
initializeTabFromUrl();
updateMergeControlsUI();
loadSortMode();
resetManualPostForm();
loadProfile();