daily bookmarks

This commit is contained in:
2025-12-04 12:56:32 +01:00
parent 839bd24309
commit 37badea913
6 changed files with 485 additions and 31 deletions

View File

@@ -3,8 +3,10 @@
const BULK_COUNT_STORAGE_KEY = 'dailyBookmarkBulkCount';
const FILTER_STORAGE_KEY = 'dailyBookmarkFilters';
const SORT_STORAGE_KEY = 'dailyBookmarkSort';
const AUTO_OPEN_STORAGE_KEY = 'dailyBookmarkAutoOpen';
const DEFAULT_BULK_COUNT = 5;
const DEFAULT_SORT = { column: 'updated_at', direction: 'desc' };
const DEFAULT_SORT = { column: 'last_completed_at', direction: 'desc' };
const AUTO_OPEN_DELAY_MS = 1500;
const state = {
dayKey: formatDayKey(new Date()),
@@ -16,7 +18,11 @@
bulkCount: loadBulkCount(),
filters: loadFilters(),
sort: loadSort(),
importing: false
importing: false,
autoOpenEnabled: loadAutoOpenEnabled(),
autoOpenTriggered: false,
autoOpenTimerId: null,
autoOpenCountdownIntervalId: null
};
let editingId = null;
@@ -33,6 +39,10 @@
const tableBody = document.getElementById('tableBody');
const bulkCountSelect = document.getElementById('bulkCountSelect');
const bulkOpenBtn = document.getElementById('bulkOpenBtn');
const autoOpenToggle = document.getElementById('autoOpenToggle');
const autoOpenOverlay = document.getElementById('autoOpenOverlay');
const autoOpenOverlayPanel = document.getElementById('autoOpenOverlayPanel');
const autoOpenCountdown = document.getElementById('autoOpenCountdown');
const openCreateBtn = document.getElementById('openCreateBtn');
const modal = document.getElementById('bookmarkModal');
@@ -50,6 +60,7 @@
const urlSuggestionBox = document.getElementById('urlSuggestionBox');
const markerInput = document.getElementById('markerInput');
const markerFilterSelect = document.getElementById('markerFilter');
const urlFilterInput = document.getElementById('urlFilter');
const resetViewBtn = document.getElementById('resetViewBtn');
const sortButtons = Array.from(document.querySelectorAll('[data-sort-key]'));
const openImportBtn = document.getElementById('openImportBtn');
@@ -140,13 +151,14 @@
if (raw) {
const parsed = JSON.parse(raw);
return {
marker: typeof parsed.marker === 'string' ? parsed.marker : ''
marker: typeof parsed.marker === 'string' ? parsed.marker : '',
url: typeof parsed.url === 'string' ? parsed.url : ''
};
}
} catch (error) {
// ignore
}
return { marker: '' };
return { marker: '', url: '' };
}
function persistFilters(filters) {
@@ -162,7 +174,7 @@
const raw = localStorage.getItem(SORT_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
const allowedColumns = ['url_template', 'marker', 'updated_at', 'last_completed_at'];
const allowedColumns = ['url_template', 'marker', 'created_at', 'updated_at', 'last_completed_at'];
const allowedDirections = ['asc', 'desc'];
if (
parsed &&
@@ -186,6 +198,22 @@
}
}
function loadAutoOpenEnabled() {
try {
return localStorage.getItem(AUTO_OPEN_STORAGE_KEY) === '1';
} catch (error) {
return false;
}
}
function persistAutoOpenEnabled(enabled) {
try {
localStorage.setItem(AUTO_OPEN_STORAGE_KEY, enabled ? '1' : '0');
} catch (error) {
// ignore
}
}
function resolveTemplate(template, dayKey) {
if (typeof template !== 'string') {
return '';
@@ -524,19 +552,44 @@
return `Vor ${diffDays} Tagen`;
}
function formatDateStamp(value) {
if (!value) return '—';
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return '—';
}
return date.toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
function normalizeMarkerValue(value) {
return (value || '').trim();
}
function getFilteredItems() {
const markerFilter = state.filters.marker || '';
const urlFilter = (state.filters.url || '').toLowerCase();
return state.items.filter((item) => {
const currentMarker = normalizeMarkerValue(item.marker).toLowerCase();
if (markerFilter === '__none') {
return !currentMarker;
if (currentMarker) {
return false;
}
} else if (markerFilter) {
if (currentMarker !== markerFilter.toLowerCase()) {
return false;
}
}
if (markerFilter) {
return currentMarker === markerFilter.toLowerCase();
if (urlFilter) {
const urlValue = (item.resolved_url || item.url_template || '').toLowerCase();
if (!urlValue.includes(urlFilter)) {
return false;
}
}
return true;
});
@@ -557,6 +610,8 @@
return a.url_template.localeCompare(b.url_template, 'de', { sensitivity: 'base' }) * direction;
case 'marker':
return normalizeMarkerValue(a.marker).localeCompare(normalizeMarkerValue(b.marker), 'de', { sensitivity: 'base' }) * direction;
case 'created_at':
return (safeTime(a.created_at) - safeTime(b.created_at)) * direction;
case 'updated_at':
return (safeTime(a.updated_at) - safeTime(b.updated_at)) * direction;
case 'last_completed_at':
@@ -678,6 +733,10 @@
}
tr.appendChild(markerTd);
const createdTd = document.createElement('td');
createdTd.textContent = formatDateStamp(item.created_at);
tr.appendChild(createdTd);
const timeTd = document.createElement('td');
timeTd.textContent = formatRelativeTimestamp(item.last_completed_at);
tr.appendChild(timeTd);
@@ -740,7 +799,7 @@
function appendSingleRow(text, className) {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = 4;
td.colSpan = 5;
td.className = className;
td.textContent = text;
tr.appendChild(td);
@@ -754,21 +813,121 @@
const visibleDone = visibleItems.filter((item) => item.completed_for_day).length;
const filterSuffix = visibleItems.length !== total ? ` · Gefiltert: ${visibleDone}/${visibleItems.length}` : '';
const text = total ? `${done}/${total} erledigt${filterSuffix}` : 'Keine Bookmarks vorhanden';
const filterParts = [];
if (state.filters.marker) {
filterParts.push(state.filters.marker === '__none' ? 'ohne Marker' : `Marker: ${state.filters.marker}`);
}
if (state.filters.url) {
filterParts.push(`URL enthält „${state.filters.url}`);
}
const filterText = filterParts.length ? ` · Filter: ${filterParts.join(' · ')}` : '';
if (heroStats) {
heroStats.textContent = text;
heroStats.textContent = `${text}${filterText}`;
}
if (listSummary) {
const markerInfo = state.filters.marker
? state.filters.marker === '__none'
? ' · Filter: ohne Marker'
: ` · Filter: ${state.filters.marker}`
: '';
listSummary.textContent = `${text} Tag ${state.dayKey}${markerInfo}`;
listSummary.textContent = `${text} Tag ${state.dayKey}${filterText}`;
}
}
function clearAutoOpenCountdown() {
if (state.autoOpenCountdownIntervalId) {
clearInterval(state.autoOpenCountdownIntervalId);
state.autoOpenCountdownIntervalId = null;
}
}
function updateAutoOpenCountdownLabel(remainingMs) {
if (!autoOpenCountdown) return;
const safeMs = Math.max(0, remainingMs);
const seconds = safeMs / 1000;
const formatted = seconds >= 10 ? seconds.toFixed(0) : seconds.toFixed(1);
autoOpenCountdown.textContent = formatted;
}
function hideAutoOpenOverlay() {
clearAutoOpenCountdown();
if (autoOpenOverlay) {
autoOpenOverlay.classList.remove('visible');
autoOpenOverlay.hidden = true;
}
}
function showAutoOpenOverlay(delayMs) {
if (!autoOpenOverlay) return;
const duration = Math.max(0, delayMs);
hideAutoOpenOverlay();
autoOpenOverlay.hidden = false;
requestAnimationFrame(() => autoOpenOverlay.classList.add('visible'));
updateAutoOpenCountdownLabel(duration);
const start = Date.now();
state.autoOpenCountdownIntervalId = setInterval(() => {
const remaining = Math.max(0, duration - (Date.now() - start));
updateAutoOpenCountdownLabel(remaining);
if (remaining <= 0) {
clearAutoOpenCountdown();
}
}, 100);
}
function cancelAutoOpen(showMessage = false) {
if (state.autoOpenTimerId) {
clearTimeout(state.autoOpenTimerId);
state.autoOpenTimerId = null;
}
state.autoOpenTriggered = false;
hideAutoOpenOverlay();
if (showMessage) {
setListStatus('Automatisches Öffnen abgebrochen.', false);
}
}
function maybeAutoOpen(reason = '', delayMs = AUTO_OPEN_DELAY_MS) {
if (!state.autoOpenEnabled) {
hideAutoOpenOverlay();
return;
}
if (state.processingBatch) return;
if (state.autoOpenTriggered) return;
const undone = getVisibleItems().filter((item) => !item.completed_for_day);
if (!undone.length) {
hideAutoOpenOverlay();
return;
}
if (state.autoOpenTimerId) {
clearTimeout(state.autoOpenTimerId);
state.autoOpenTimerId = null;
}
hideAutoOpenOverlay();
state.autoOpenTriggered = true;
const delay = typeof delayMs === 'number' ? Math.max(0, delayMs) : AUTO_OPEN_DELAY_MS;
if (delay === 0) {
if (state.autoOpenEnabled) {
openBatch({ auto: true });
} else {
state.autoOpenTriggered = false;
}
return;
}
showAutoOpenOverlay(delay);
state.autoOpenTimerId = setTimeout(() => {
state.autoOpenTimerId = null;
hideAutoOpenOverlay();
if (state.autoOpenEnabled) {
openBatch({ auto: true });
} else {
state.autoOpenTriggered = false;
}
}, delay);
}
async function loadDailyBookmarks() {
state.loading = true;
if (state.autoOpenTimerId) {
clearTimeout(state.autoOpenTimerId);
state.autoOpenTimerId = null;
}
hideAutoOpenOverlay();
state.autoOpenTriggered = false;
state.error = '';
setListStatus('');
renderTable();
@@ -779,6 +938,7 @@
updateHeroStats();
renderMarkerFilterOptions();
renderTable();
maybeAutoOpen('load');
} catch (error) {
state.loading = false;
state.error = 'Konnte Bookmarks nicht laden.';
@@ -897,11 +1057,16 @@
}
}
async function openBatch() {
async function openBatch({ auto = false } = {}) {
if (state.processingBatch) return;
if (!auto) {
cancelAutoOpen(false);
}
const undone = getVisibleItems().filter((item) => !item.completed_for_day);
if (!undone.length) {
setListStatus('Keine offenen Bookmarks für den gewählten Tag.', true);
if (!auto) {
setListStatus('Keine offenen Bookmarks für den gewählten Tag.', true);
}
return;
}
@@ -909,8 +1074,14 @@
const selection = undone.slice(0, count);
state.processingBatch = true;
bulkOpenBtn.disabled = true;
setListStatus('');
if (bulkOpenBtn) {
bulkOpenBtn.disabled = true;
}
if (!auto) {
setListStatus('');
} else {
setListStatus(`Öffne automatisch ${selection.length} Links...`, false);
}
for (const item of selection) {
const target = item.resolved_url || item.url_template;
@@ -929,7 +1100,12 @@
}
state.processingBatch = false;
bulkOpenBtn.disabled = false;
if (bulkOpenBtn) {
bulkOpenBtn.disabled = false;
}
if (auto) {
setListStatus('');
}
updateHeroStats();
renderTable();
}
@@ -1097,8 +1273,38 @@
});
}
if (bulkOpenBtn) {
bulkOpenBtn.addEventListener('click', openBatch);
bulkOpenBtn.addEventListener('click', () => openBatch());
}
if (autoOpenOverlayPanel) {
autoOpenOverlayPanel.addEventListener('click', () => cancelAutoOpen(true));
}
if (autoOpenToggle) {
autoOpenToggle.checked = !!state.autoOpenEnabled;
autoOpenToggle.addEventListener('change', () => {
state.autoOpenEnabled = autoOpenToggle.checked;
persistAutoOpenEnabled(state.autoOpenEnabled);
state.autoOpenTriggered = false;
if (!state.autoOpenEnabled && state.autoOpenTimerId) {
cancelAutoOpen(false);
}
if (state.autoOpenEnabled) {
maybeAutoOpen('toggle');
} else {
hideAutoOpenOverlay();
}
});
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible' && state.autoOpenEnabled) {
// Reset the guard so that returning to the tab can trigger the next batch.
if (state.autoOpenTimerId) {
clearTimeout(state.autoOpenTimerId);
state.autoOpenTimerId = null;
}
state.autoOpenTriggered = false;
maybeAutoOpen('visibility', AUTO_OPEN_DELAY_MS);
}
});
if (markerFilterSelect) {
markerFilterSelect.addEventListener('change', () => {
state.filters.marker = markerFilterSelect.value || '';
@@ -1107,13 +1313,25 @@
renderTable();
});
}
if (urlFilterInput) {
urlFilterInput.value = state.filters.url || '';
urlFilterInput.addEventListener('input', () => {
state.filters.url = urlFilterInput.value.trim();
persistFilters(state.filters);
updateHeroStats();
renderTable();
});
}
if (resetViewBtn) {
resetViewBtn.addEventListener('click', () => {
state.filters = { marker: '' };
state.filters = { marker: '', url: '' };
state.sort = { ...DEFAULT_SORT };
persistFilters(state.filters);
persistSort(state.sort);
renderMarkerFilterOptions();
if (urlFilterInput) {
urlFilterInput.value = '';
}
updateHeroStats();
renderTable();
});
@@ -1126,7 +1344,7 @@
if (state.sort.column === key) {
state.sort.direction = state.sort.direction === 'asc' ? 'desc' : 'asc';
} else {
const defaultDirection = key === 'last_completed_at' || key === 'updated_at' ? 'desc' : 'asc';
const defaultDirection = ['last_completed_at', 'updated_at', 'created_at'].includes(key) ? 'desc' : 'asc';
state.sort = { column: key, direction: defaultDirection };
}
persistSort(state.sort);