daily bookmarks
This commit is contained in:
257
web/app.js
257
web/app.js
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user