1388 lines
42 KiB
JavaScript
1388 lines
42 KiB
JavaScript
(function () {
|
||
const API_URL = window.API_URL || 'https://fb.srv.medeba-media.de/api';
|
||
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: 'last_completed_at', direction: 'desc' };
|
||
const AUTO_OPEN_DELAY_MS = 1500;
|
||
|
||
const state = {
|
||
dayKey: formatDayKey(new Date()),
|
||
items: [],
|
||
loading: false,
|
||
saving: false,
|
||
processingBatch: false,
|
||
error: '',
|
||
bulkCount: loadBulkCount(),
|
||
filters: loadFilters(),
|
||
sort: loadSort(),
|
||
importing: false,
|
||
autoOpenEnabled: loadAutoOpenEnabled(),
|
||
autoOpenTriggered: false,
|
||
autoOpenTimerId: null,
|
||
autoOpenCountdownIntervalId: null
|
||
};
|
||
|
||
let editingId = null;
|
||
|
||
const dayLabel = document.getElementById('dayLabel');
|
||
const daySubLabel = document.getElementById('daySubLabel');
|
||
const prevDayBtn = document.getElementById('prevDayBtn');
|
||
const nextDayBtn = document.getElementById('nextDayBtn');
|
||
const todayBtn = document.getElementById('todayBtn');
|
||
const refreshBtn = document.getElementById('refreshBtn');
|
||
const heroStats = document.getElementById('heroStats');
|
||
const listSummary = document.getElementById('listSummary');
|
||
const listStatus = document.getElementById('listStatus');
|
||
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');
|
||
const modalCloseBtn = document.getElementById('modalCloseBtn');
|
||
const modalBackdrop = modal ? modal.querySelector('.modal__backdrop') : null;
|
||
const formEl = document.getElementById('bookmarkForm');
|
||
const titleInput = document.getElementById('titleInput');
|
||
const urlInput = document.getElementById('urlInput');
|
||
const notesInput = document.getElementById('notesInput');
|
||
const resetBtn = document.getElementById('resetBtn');
|
||
const submitBtn = document.getElementById('submitBtn');
|
||
const previewLink = document.getElementById('previewLink');
|
||
const formStatus = document.getElementById('formStatus');
|
||
const formModeLabel = document.getElementById('formModeLabel');
|
||
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');
|
||
const importModal = document.getElementById('importModal');
|
||
const importCloseBtn = document.getElementById('importCloseBtn');
|
||
const importBackdrop = importModal ? importModal.querySelector('.modal__backdrop') : null;
|
||
const importForm = document.getElementById('importForm');
|
||
const importInput = document.getElementById('importInput');
|
||
const importMarkerInput = document.getElementById('importMarkerInput');
|
||
const importResetBtn = document.getElementById('importResetBtn');
|
||
const importSubmitBtn = document.getElementById('importSubmitBtn');
|
||
const importStatus = document.getElementById('importStatus');
|
||
|
||
const PLACEHOLDER_PATTERN = /\{\{\s*([^}]+)\s*\}\}/gi;
|
||
|
||
function formatDayKey(date) {
|
||
const d = date instanceof Date ? date : new Date();
|
||
const year = d.getFullYear();
|
||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||
const day = String(d.getDate()).padStart(2, '0');
|
||
return `${year}-${month}-${day}`;
|
||
}
|
||
|
||
function parseDayKey(dayKey) {
|
||
if (typeof dayKey !== 'string') {
|
||
return new Date();
|
||
}
|
||
const parts = dayKey.split('-').map((part) => parseInt(part, 10));
|
||
if (parts.length !== 3 || parts.some(Number.isNaN)) {
|
||
return new Date();
|
||
}
|
||
return new Date(parts[0], parts[1] - 1, parts[2]);
|
||
}
|
||
|
||
function addDays(date, delta) {
|
||
const base = date instanceof Date ? date : new Date();
|
||
const next = new Date(base);
|
||
next.setDate(base.getDate() + delta);
|
||
return next;
|
||
}
|
||
|
||
function formatDayLabel(dayKey) {
|
||
const date = parseDayKey(dayKey);
|
||
return date.toLocaleDateString('de-DE', {
|
||
weekday: 'long',
|
||
day: '2-digit',
|
||
month: 'long',
|
||
year: 'numeric'
|
||
});
|
||
}
|
||
|
||
function formatRelativeDay(dayKey) {
|
||
const target = parseDayKey(dayKey);
|
||
const today = new Date();
|
||
const todayKey = formatDayKey(today);
|
||
const diff = Math.round((target.setHours(0, 0, 0, 0) - new Date(todayKey).setHours(0, 0, 0, 0)) / 86400000);
|
||
if (diff === 0) return 'Heute';
|
||
if (diff === 1) return 'Morgen';
|
||
if (diff === -1) return 'Gestern';
|
||
if (diff > 1) return `In ${diff} Tagen`;
|
||
return `${Math.abs(diff)} Tage her`;
|
||
}
|
||
|
||
function loadBulkCount() {
|
||
try {
|
||
const stored = localStorage.getItem(BULK_COUNT_STORAGE_KEY);
|
||
const value = parseInt(stored, 10);
|
||
if (!Number.isNaN(value) && [1, 5, 10, 15, 20].includes(value)) {
|
||
return value;
|
||
}
|
||
} catch (error) {
|
||
// ignore
|
||
}
|
||
return DEFAULT_BULK_COUNT;
|
||
}
|
||
|
||
function persistBulkCount(value) {
|
||
try {
|
||
localStorage.setItem(BULK_COUNT_STORAGE_KEY, String(value));
|
||
} catch (error) {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
function loadFilters() {
|
||
try {
|
||
const raw = localStorage.getItem(FILTER_STORAGE_KEY);
|
||
if (raw) {
|
||
const parsed = JSON.parse(raw);
|
||
return {
|
||
marker: typeof parsed.marker === 'string' ? parsed.marker : '',
|
||
url: typeof parsed.url === 'string' ? parsed.url : ''
|
||
};
|
||
}
|
||
} catch (error) {
|
||
// ignore
|
||
}
|
||
return { marker: '', url: '' };
|
||
}
|
||
|
||
function persistFilters(filters) {
|
||
try {
|
||
localStorage.setItem(FILTER_STORAGE_KEY, JSON.stringify(filters || {}));
|
||
} catch (error) {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
function loadSort() {
|
||
try {
|
||
const raw = localStorage.getItem(SORT_STORAGE_KEY);
|
||
if (raw) {
|
||
const parsed = JSON.parse(raw);
|
||
const allowedColumns = ['url_template', 'marker', 'created_at', 'updated_at', 'last_completed_at'];
|
||
const allowedDirections = ['asc', 'desc'];
|
||
if (
|
||
parsed &&
|
||
allowedColumns.includes(parsed.column) &&
|
||
allowedDirections.includes(parsed.direction)
|
||
) {
|
||
return parsed;
|
||
}
|
||
}
|
||
} catch (error) {
|
||
// ignore
|
||
}
|
||
return { ...DEFAULT_SORT };
|
||
}
|
||
|
||
function persistSort(sort) {
|
||
try {
|
||
localStorage.setItem(SORT_STORAGE_KEY, JSON.stringify(sort || DEFAULT_SORT));
|
||
} catch (error) {
|
||
// ignore
|
||
}
|
||
}
|
||
|
||
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 '';
|
||
}
|
||
const baseDate = parseDayKey(dayKey);
|
||
return template.replace(PLACEHOLDER_PATTERN, (_match, contentRaw) => {
|
||
const content = (contentRaw || '').trim();
|
||
if (!content) {
|
||
return '';
|
||
}
|
||
|
||
const counterMatch = content.match(/^counter:\s*([+-]?\d+)([+-]\d+)?$/i);
|
||
if (counterMatch) {
|
||
const base = parseInt(counterMatch[1], 10);
|
||
const offset = counterMatch[2] ? parseInt(counterMatch[2], 10) || 0 : 0;
|
||
if (Number.isNaN(base)) {
|
||
return '';
|
||
}
|
||
const date = addDays(baseDate, offset);
|
||
return String(base + date.getDate());
|
||
}
|
||
|
||
const placeholderMatch = content.match(/^(date|day|dd|mm|month|yyyy|yy)([+-]\d+)?$/i);
|
||
if (!placeholderMatch) {
|
||
return content;
|
||
}
|
||
|
||
const token = String(placeholderMatch[1] || '').toLowerCase();
|
||
const offset = placeholderMatch[2] ? parseInt(placeholderMatch[2], 10) || 0 : 0;
|
||
const date = addDays(baseDate, offset);
|
||
|
||
switch (token) {
|
||
case 'date':
|
||
return formatDayKey(date);
|
||
case 'day':
|
||
return String(date.getDate());
|
||
case 'dd':
|
||
return String(date.getDate()).padStart(2, '0');
|
||
case 'month':
|
||
case 'mm':
|
||
return String(date.getMonth() + 1).padStart(2, '0');
|
||
case 'yyyy':
|
||
return String(date.getFullYear());
|
||
case 'yy':
|
||
return String(date.getFullYear()).slice(-2);
|
||
default:
|
||
return token;
|
||
}
|
||
});
|
||
}
|
||
|
||
function isDelimiter(char) {
|
||
return !char || /[\\/_\\-\\.\\?#&=]/.test(char);
|
||
}
|
||
|
||
function buildUrlSuggestions(urlValue) {
|
||
const suggestions = [];
|
||
if (!urlValue || typeof urlValue !== 'string') {
|
||
return suggestions;
|
||
}
|
||
|
||
const raw = urlValue.trim();
|
||
if (!raw) {
|
||
return suggestions;
|
||
}
|
||
|
||
const today = parseDayKey(state.dayKey);
|
||
const day = today.getDate();
|
||
const yearFull = String(today.getFullYear());
|
||
const yearShort = yearFull.slice(-2);
|
||
const monthNumber = today.getMonth() + 1;
|
||
|
||
const tokens = [];
|
||
const addToken = (text, placeholder) => {
|
||
if (!text) return;
|
||
tokens.push({ text, placeholder });
|
||
};
|
||
|
||
const dayText = String(day);
|
||
addToken(dayText, '{{day}}');
|
||
const dayPadded = dayText.padStart(2, '0');
|
||
if (dayPadded !== dayText) {
|
||
addToken(dayPadded, '{{dd}}');
|
||
}
|
||
|
||
const monthText = String(monthNumber);
|
||
addToken(monthText, '{{mm}}');
|
||
const monthPadded = monthText.padStart(2, '0');
|
||
if (monthPadded !== monthText) {
|
||
addToken(monthPadded, '{{mm}}');
|
||
}
|
||
|
||
addToken(yearFull, '{{yyyy}}');
|
||
addToken(yearShort, '{{yy}}');
|
||
|
||
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||
|
||
function findOccurrences(text, tokenText) {
|
||
const pattern = new RegExp(`(^|[^0-9])(${escapeRegex(tokenText)})(?=$|[^0-9])`, 'g');
|
||
const occurrences = [];
|
||
let match;
|
||
while ((match = pattern.exec(text)) !== null) {
|
||
const prefixLength = (match[1] || '').length;
|
||
const startIndex = match.index + prefixLength;
|
||
occurrences.push({ index: startIndex, length: tokenText.length });
|
||
}
|
||
return occurrences;
|
||
}
|
||
|
||
for (const token of tokens) {
|
||
const occ = findOccurrences(raw, token.text);
|
||
if (!occ.length) {
|
||
continue;
|
||
}
|
||
const candidate = occ[occ.length - 1]; // prefer last occurrence (z.B. am URL-Ende)
|
||
const replaced = `${raw.slice(0, candidate.index)}${token.placeholder}${raw.slice(candidate.index + candidate.length)}`;
|
||
const label = `Ersetze „${token.text}“ durch ${token.placeholder}`;
|
||
if (!suggestions.some((s) => s.label === label && s.value === replaced)) {
|
||
suggestions.push({
|
||
label,
|
||
value: replaced
|
||
});
|
||
}
|
||
}
|
||
|
||
return suggestions;
|
||
}
|
||
|
||
function renderUrlSuggestions() {
|
||
if (!urlSuggestionBox) {
|
||
return;
|
||
}
|
||
const suggestions = buildUrlSuggestions(urlInput ? urlInput.value : '');
|
||
urlSuggestionBox.innerHTML = '';
|
||
if (!suggestions.length) {
|
||
urlSuggestionBox.hidden = true;
|
||
return;
|
||
}
|
||
|
||
const text = document.createElement('span');
|
||
text.className = 'suggestion-box__text';
|
||
text.textContent = 'Mögliche Platzhalter:';
|
||
urlSuggestionBox.appendChild(text);
|
||
|
||
const applySuggestion = (value) => {
|
||
if (!urlInput) {
|
||
return;
|
||
}
|
||
urlInput.value = value;
|
||
updatePreviewLink();
|
||
renderUrlSuggestions();
|
||
const end = urlInput.value.length;
|
||
urlInput.focus();
|
||
try {
|
||
urlInput.setSelectionRange(end, end);
|
||
} catch (error) {
|
||
// ignore if not supported
|
||
}
|
||
};
|
||
|
||
suggestions.forEach((sugg) => {
|
||
const item = document.createElement('div');
|
||
item.className = 'suggestion-box__item';
|
||
|
||
const btn = document.createElement('button');
|
||
btn.type = 'button';
|
||
btn.className = 'suggestion-btn';
|
||
btn.textContent = sugg.label;
|
||
btn.addEventListener('click', () => {
|
||
applySuggestion(sugg.value);
|
||
});
|
||
item.appendChild(btn);
|
||
|
||
const preview = document.createElement('a');
|
||
preview.className = 'suggestion-preview';
|
||
preview.href = resolveTemplate(sugg.value, state.dayKey) || sugg.value;
|
||
preview.target = '_blank';
|
||
preview.rel = 'noopener';
|
||
preview.title = resolveTemplate(sugg.value, state.dayKey) || sugg.value;
|
||
preview.textContent = resolveTemplate(sugg.value, state.dayKey) || sugg.value;
|
||
preview.addEventListener('click', (event) => {
|
||
event.preventDefault();
|
||
applySuggestion(sugg.value);
|
||
});
|
||
item.appendChild(preview);
|
||
|
||
urlSuggestionBox.appendChild(item);
|
||
});
|
||
|
||
urlSuggestionBox.hidden = false;
|
||
}
|
||
|
||
async function apiFetch(url, options = {}) {
|
||
const response = await fetch(url, {
|
||
...options,
|
||
credentials: 'include',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...(options.headers || {})
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const message = `Fehler: HTTP ${response.status}`;
|
||
throw new Error(message);
|
||
}
|
||
|
||
if (response.status === 204) {
|
||
return null;
|
||
}
|
||
|
||
const text = await response.text();
|
||
if (!text) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
return JSON.parse(text);
|
||
} catch (error) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
function updateDayUI() {
|
||
if (dayLabel) {
|
||
dayLabel.textContent = formatDayLabel(state.dayKey);
|
||
}
|
||
if (daySubLabel) {
|
||
daySubLabel.textContent = formatRelativeDay(state.dayKey);
|
||
}
|
||
updatePreviewLink();
|
||
}
|
||
|
||
function setDayKey(dayKey) {
|
||
state.dayKey = formatDayKey(parseDayKey(dayKey));
|
||
updateDayUI();
|
||
loadDailyBookmarks();
|
||
}
|
||
|
||
function setFormStatus(message, isError = false) {
|
||
if (!formStatus) {
|
||
return;
|
||
}
|
||
formStatus.textContent = message || '';
|
||
formStatus.classList.toggle('form-status--error', !!isError);
|
||
}
|
||
|
||
function setListStatus(message, isError = false) {
|
||
if (!listStatus) {
|
||
return;
|
||
}
|
||
listStatus.textContent = message || '';
|
||
listStatus.classList.toggle('list-status--error', !!isError);
|
||
}
|
||
|
||
function updatePreviewLink() {
|
||
if (!previewLink || !urlInput) {
|
||
return;
|
||
}
|
||
const resolved = resolveTemplate(urlInput.value || '', state.dayKey);
|
||
previewLink.textContent = resolved || '–';
|
||
if (resolved) {
|
||
previewLink.href = resolved;
|
||
previewLink.target = '_blank';
|
||
previewLink.rel = 'noopener';
|
||
} else {
|
||
previewLink.removeAttribute('href');
|
||
}
|
||
renderUrlSuggestions();
|
||
}
|
||
|
||
function resetForm() {
|
||
editingId = null;
|
||
formModeLabel.textContent = 'Neues Bookmark';
|
||
submitBtn.textContent = 'Speichern';
|
||
formEl.reset();
|
||
if (markerInput) {
|
||
markerInput.value = '';
|
||
}
|
||
setFormStatus('');
|
||
updatePreviewLink();
|
||
renderUrlSuggestions();
|
||
}
|
||
|
||
function openModal(mode, bookmark) {
|
||
if (importModal && !importModal.hidden) {
|
||
closeImportModal();
|
||
}
|
||
if (mode === 'edit' && bookmark) {
|
||
editingId = bookmark.id;
|
||
formModeLabel.textContent = 'Bookmark bearbeiten';
|
||
submitBtn.textContent = 'Aktualisieren';
|
||
titleInput.value = bookmark.title || '';
|
||
urlInput.value = bookmark.url_template || '';
|
||
notesInput.value = bookmark.notes || '';
|
||
if (markerInput) {
|
||
markerInput.value = bookmark.marker || '';
|
||
}
|
||
setFormStatus('Bearbeite vorhandenes Bookmark');
|
||
} else {
|
||
resetForm();
|
||
}
|
||
|
||
if (modal) {
|
||
modal.hidden = false;
|
||
modal.focus();
|
||
}
|
||
updatePreviewLink();
|
||
if (mode === 'edit' && titleInput) {
|
||
titleInput.focus();
|
||
} else if (urlInput) {
|
||
urlInput.focus();
|
||
}
|
||
}
|
||
|
||
function closeModal() {
|
||
if (modal) {
|
||
modal.hidden = true;
|
||
}
|
||
resetForm();
|
||
}
|
||
|
||
function formatRelativeTimestamp(value) {
|
||
if (!value) return 'Noch nie erledigt';
|
||
const date = new Date(value);
|
||
if (Number.isNaN(date.getTime())) {
|
||
return 'Zeitpunkt unbekannt';
|
||
}
|
||
const diffMs = Date.now() - date.getTime();
|
||
const diffMin = Math.floor(diffMs / 60000);
|
||
if (diffMin < 1) return 'Gerade erledigt';
|
||
if (diffMin < 60) return `Vor ${diffMin} Min`;
|
||
const diffHours = Math.floor(diffMin / 60);
|
||
if (diffHours < 24) return `Vor ${diffHours} Std`;
|
||
const diffDays = Math.floor(diffHours / 24);
|
||
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') {
|
||
if (currentMarker) {
|
||
return false;
|
||
}
|
||
} else if (markerFilter) {
|
||
if (currentMarker !== markerFilter.toLowerCase()) {
|
||
return false;
|
||
}
|
||
}
|
||
if (urlFilter) {
|
||
const urlValue = (item.resolved_url || item.url_template || '').toLowerCase();
|
||
if (!urlValue.includes(urlFilter)) {
|
||
return false;
|
||
}
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
|
||
function sortItems(items) {
|
||
const sorted = [...items];
|
||
const activeSort = state.sort || DEFAULT_SORT;
|
||
const direction = activeSort.direction === 'asc' ? 1 : -1;
|
||
const safeTime = (value) => {
|
||
const time = value ? new Date(value).getTime() : 0;
|
||
return Number.isNaN(time) ? 0 : time;
|
||
};
|
||
|
||
sorted.sort((a, b) => {
|
||
switch (activeSort.column) {
|
||
case 'url_template':
|
||
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':
|
||
default:
|
||
return (safeTime(a.last_completed_at) - safeTime(b.last_completed_at)) * direction;
|
||
}
|
||
});
|
||
|
||
return sorted;
|
||
}
|
||
|
||
function getVisibleItems() {
|
||
return sortItems(getFilteredItems());
|
||
}
|
||
|
||
function updateSortIndicators() {
|
||
if (!sortButtons || !sortButtons.length) {
|
||
return;
|
||
}
|
||
const activeSort = state.sort || DEFAULT_SORT;
|
||
sortButtons.forEach((btn) => {
|
||
const key = btn.dataset.sortKey;
|
||
const isActive = key === activeSort.column;
|
||
btn.classList.toggle('is-active', isActive);
|
||
btn.setAttribute('data-sort-direction', isActive ? (activeSort.direction === 'asc' ? '↑' : '↓') : '↕');
|
||
});
|
||
}
|
||
|
||
function renderMarkerFilterOptions() {
|
||
if (!markerFilterSelect) {
|
||
return;
|
||
}
|
||
const activeValue = state.filters.marker || '';
|
||
const markers = Array.from(
|
||
new Set(
|
||
state.items
|
||
.map((item) => normalizeMarkerValue(item.marker))
|
||
.filter(Boolean)
|
||
)
|
||
).sort((a, b) => a.localeCompare(b, 'de', { sensitivity: 'base' }));
|
||
|
||
markerFilterSelect.innerHTML = '';
|
||
const addOption = (value, label) => {
|
||
const opt = document.createElement('option');
|
||
opt.value = value;
|
||
opt.textContent = label;
|
||
markerFilterSelect.appendChild(opt);
|
||
};
|
||
|
||
addOption('', 'Alle Marker');
|
||
addOption('__none', 'Ohne Marker');
|
||
markers.forEach((marker) => addOption(marker, marker));
|
||
|
||
if (activeValue && activeValue !== '__none' && !markers.includes(activeValue)) {
|
||
addOption(activeValue, `${activeValue} (kein Treffer)`);
|
||
}
|
||
|
||
const nextValue = activeValue || '';
|
||
markerFilterSelect.value = nextValue;
|
||
state.filters.marker = nextValue;
|
||
persistFilters(state.filters);
|
||
}
|
||
|
||
function renderTable() {
|
||
if (!tableBody) {
|
||
return;
|
||
}
|
||
|
||
tableBody.innerHTML = '';
|
||
updateSortIndicators();
|
||
|
||
if (state.loading) {
|
||
appendSingleRow('Lade Bookmarks...', 'loading-state');
|
||
return;
|
||
}
|
||
|
||
if (state.error) {
|
||
appendSingleRow(state.error, 'error-state');
|
||
return;
|
||
}
|
||
|
||
if (!state.items.length) {
|
||
appendSingleRow('Keine Bookmarks vorhanden. Lege dein erstes Bookmark an.', 'empty-state');
|
||
return;
|
||
}
|
||
|
||
const visibleItems = getVisibleItems();
|
||
if (!visibleItems.length) {
|
||
appendSingleRow('Keine Bookmarks für diesen Filter.', 'empty-state');
|
||
return;
|
||
}
|
||
|
||
visibleItems.forEach((item) => {
|
||
const tr = document.createElement('tr');
|
||
tr.classList.add(item.completed_for_day ? 'is-done' : 'is-open');
|
||
|
||
const urlTd = document.createElement('td');
|
||
urlTd.className = 'url-cell';
|
||
const link = document.createElement('a');
|
||
const resolvedUrl = item.resolved_url || item.url_template;
|
||
link.href = resolvedUrl;
|
||
link.target = '_blank';
|
||
link.rel = 'noopener';
|
||
link.title = resolvedUrl;
|
||
link.textContent = resolvedUrl;
|
||
urlTd.appendChild(link);
|
||
tr.appendChild(urlTd);
|
||
|
||
const markerTd = document.createElement('td');
|
||
markerTd.className = 'marker-cell';
|
||
if (normalizeMarkerValue(item.marker)) {
|
||
const markerChip = document.createElement('span');
|
||
markerChip.className = 'chip chip--marker';
|
||
markerChip.textContent = item.marker;
|
||
markerTd.appendChild(markerChip);
|
||
} else {
|
||
markerTd.textContent = '–';
|
||
markerTd.classList.add('muted');
|
||
}
|
||
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);
|
||
|
||
const actionsTd = document.createElement('td');
|
||
actionsTd.className = 'table-actions';
|
||
|
||
const openBtn = document.createElement('button');
|
||
openBtn.className = 'ghost-btn';
|
||
openBtn.type = 'button';
|
||
openBtn.textContent = '🔗';
|
||
openBtn.title = 'Öffnen';
|
||
openBtn.addEventListener('click', () => {
|
||
const target = item.resolved_url || item.url_template;
|
||
if (target) {
|
||
window.open(target, '_blank', 'noopener');
|
||
}
|
||
});
|
||
actionsTd.appendChild(openBtn);
|
||
|
||
const toggleBtn = document.createElement('button');
|
||
toggleBtn.className = 'ghost-btn';
|
||
toggleBtn.type = 'button';
|
||
toggleBtn.textContent = item.completed_for_day ? '↩️' : '✅';
|
||
toggleBtn.title = item.completed_for_day ? 'Zurücksetzen' : 'Heute erledigt';
|
||
toggleBtn.addEventListener('click', () => {
|
||
if (item.completed_for_day) {
|
||
undoDailyBookmark(item.id);
|
||
} else {
|
||
completeDailyBookmark(item.id);
|
||
}
|
||
});
|
||
actionsTd.appendChild(toggleBtn);
|
||
|
||
const editBtn = document.createElement('button');
|
||
editBtn.className = 'ghost-btn';
|
||
editBtn.type = 'button';
|
||
editBtn.textContent = '✏️';
|
||
editBtn.title = 'Bearbeiten';
|
||
editBtn.addEventListener('click', () => openModal('edit', item));
|
||
actionsTd.appendChild(editBtn);
|
||
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.className = 'ghost-btn danger';
|
||
deleteBtn.type = 'button';
|
||
deleteBtn.textContent = '🗑️';
|
||
deleteBtn.title = 'Löschen';
|
||
deleteBtn.addEventListener('click', () => {
|
||
if (window.confirm('Bookmark wirklich löschen?')) {
|
||
deleteDailyBookmark(item.id);
|
||
}
|
||
});
|
||
actionsTd.appendChild(deleteBtn);
|
||
|
||
tr.appendChild(actionsTd);
|
||
tableBody.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
function appendSingleRow(text, className) {
|
||
const tr = document.createElement('tr');
|
||
const td = document.createElement('td');
|
||
td.colSpan = 5;
|
||
td.className = className;
|
||
td.textContent = text;
|
||
tr.appendChild(td);
|
||
tableBody.appendChild(tr);
|
||
}
|
||
|
||
function updateHeroStats() {
|
||
const total = state.items.length;
|
||
const done = state.items.filter((item) => item.completed_for_day).length;
|
||
const visibleItems = getFilteredItems();
|
||
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}${filterText}`;
|
||
}
|
||
if (listSummary) {
|
||
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();
|
||
try {
|
||
const data = await apiFetch(`${API_URL}/daily-bookmarks?day=${encodeURIComponent(state.dayKey)}`);
|
||
state.items = Array.isArray(data) ? data : [];
|
||
state.loading = false;
|
||
updateHeroStats();
|
||
renderMarkerFilterOptions();
|
||
renderTable();
|
||
maybeAutoOpen('load');
|
||
} catch (error) {
|
||
state.loading = false;
|
||
state.error = 'Konnte Bookmarks nicht laden.';
|
||
setListStatus(state.error, true);
|
||
renderTable();
|
||
}
|
||
}
|
||
|
||
async function completeDailyBookmark(id) {
|
||
if (!id) return;
|
||
state.error = '';
|
||
try {
|
||
const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}/check`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ day: state.dayKey })
|
||
});
|
||
state.items = state.items.map((item) => (item.id === id ? updated : item));
|
||
updateHeroStats();
|
||
renderTable();
|
||
} catch (error) {
|
||
state.error = 'Konnte nicht abhaken.';
|
||
setListStatus(state.error, true);
|
||
renderTable();
|
||
}
|
||
}
|
||
|
||
async function undoDailyBookmark(id) {
|
||
if (!id) return;
|
||
state.error = '';
|
||
try {
|
||
const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}/check?day=${encodeURIComponent(state.dayKey)}`, {
|
||
method: 'DELETE'
|
||
});
|
||
state.items = state.items.map((item) => (item.id === id ? updated : item));
|
||
updateHeroStats();
|
||
renderTable();
|
||
} catch (error) {
|
||
state.error = 'Konnte nicht zurücksetzen.';
|
||
setListStatus(state.error, true);
|
||
renderTable();
|
||
}
|
||
}
|
||
|
||
async function deleteDailyBookmark(id) {
|
||
if (!id) return;
|
||
state.error = '';
|
||
try {
|
||
await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(id)}`, {
|
||
method: 'DELETE'
|
||
});
|
||
state.items = state.items.filter((item) => item.id !== id);
|
||
if (editingId === id) {
|
||
resetForm();
|
||
}
|
||
updateHeroStats();
|
||
renderMarkerFilterOptions();
|
||
renderTable();
|
||
} catch (error) {
|
||
state.error = 'Konnte Bookmark nicht löschen.';
|
||
setListStatus(state.error, true);
|
||
renderTable();
|
||
}
|
||
}
|
||
|
||
async function submitForm(event) {
|
||
event.preventDefault();
|
||
if (state.saving) return;
|
||
|
||
const title = titleInput.value.trim();
|
||
const url = urlInput.value.trim();
|
||
const notes = notesInput.value.trim();
|
||
const marker = markerInput ? markerInput.value.trim() : '';
|
||
|
||
if (!url) {
|
||
setFormStatus('URL-Template ist Pflicht.', true);
|
||
urlInput.focus();
|
||
return;
|
||
}
|
||
|
||
state.saving = true;
|
||
submitBtn.disabled = true;
|
||
setFormStatus('');
|
||
|
||
const payload = {
|
||
title,
|
||
url_template: url,
|
||
notes,
|
||
marker,
|
||
day: state.dayKey
|
||
};
|
||
|
||
const targetUrl = editingId
|
||
? `${API_URL}/daily-bookmarks/${encodeURIComponent(editingId)}`
|
||
: `${API_URL}/daily-bookmarks`;
|
||
const method = editingId ? 'PUT' : 'POST';
|
||
|
||
try {
|
||
const saved = await apiFetch(targetUrl, {
|
||
method,
|
||
body: JSON.stringify(payload)
|
||
});
|
||
if (editingId) {
|
||
state.items = state.items.map((item) => (item.id === editingId ? saved : item));
|
||
} else {
|
||
state.items = [saved, ...state.items];
|
||
}
|
||
updateHeroStats();
|
||
renderMarkerFilterOptions();
|
||
renderTable();
|
||
closeModal();
|
||
} catch (error) {
|
||
setFormStatus('Speichern fehlgeschlagen.', true);
|
||
} finally {
|
||
state.saving = false;
|
||
submitBtn.disabled = false;
|
||
}
|
||
}
|
||
|
||
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) {
|
||
if (!auto) {
|
||
setListStatus('Keine offenen Bookmarks für den gewählten Tag.', true);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const count = state.bulkCount || DEFAULT_BULK_COUNT;
|
||
const selection = undone.slice(0, count);
|
||
|
||
state.processingBatch = true;
|
||
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;
|
||
if (target) {
|
||
window.open(target, '_blank', 'noopener');
|
||
}
|
||
try {
|
||
const updated = await apiFetch(`${API_URL}/daily-bookmarks/${encodeURIComponent(item.id)}/check`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ day: state.dayKey })
|
||
});
|
||
state.items = state.items.map((entry) => (entry.id === item.id ? updated : entry));
|
||
} catch (error) {
|
||
setListStatus('Einige Bookmarks konnten nicht abgehakt werden.', true);
|
||
}
|
||
}
|
||
|
||
state.processingBatch = false;
|
||
if (bulkOpenBtn) {
|
||
bulkOpenBtn.disabled = false;
|
||
}
|
||
if (auto) {
|
||
setListStatus('');
|
||
}
|
||
updateHeroStats();
|
||
renderTable();
|
||
}
|
||
|
||
function setImportStatus(message, isError = false) {
|
||
if (!importStatus) {
|
||
return;
|
||
}
|
||
importStatus.textContent = message || '';
|
||
importStatus.classList.toggle('form-status--error', !!isError);
|
||
}
|
||
|
||
function resetImportForm() {
|
||
if (importForm) {
|
||
importForm.reset();
|
||
}
|
||
const suggestedMarker = state.filters.marker && state.filters.marker !== '__none' ? state.filters.marker : '';
|
||
if (importMarkerInput) {
|
||
importMarkerInput.value = suggestedMarker;
|
||
}
|
||
setImportStatus('');
|
||
}
|
||
|
||
function openImportModal() {
|
||
if (modal && !modal.hidden) {
|
||
closeModal();
|
||
}
|
||
resetImportForm();
|
||
if (importModal) {
|
||
importModal.hidden = false;
|
||
importModal.focus();
|
||
}
|
||
if (importInput) {
|
||
importInput.focus();
|
||
}
|
||
}
|
||
|
||
function closeImportModal() {
|
||
if (importModal) {
|
||
importModal.hidden = true;
|
||
}
|
||
resetImportForm();
|
||
}
|
||
|
||
async function submitImportForm(event) {
|
||
event.preventDefault();
|
||
if (state.importing) return;
|
||
|
||
const rawText = importInput ? importInput.value.trim() : '';
|
||
const marker = importMarkerInput ? importMarkerInput.value.trim() : '';
|
||
if (!rawText) {
|
||
setImportStatus('Bitte füge mindestens eine URL ein.', true);
|
||
if (importInput) {
|
||
importInput.focus();
|
||
}
|
||
return;
|
||
}
|
||
|
||
const urls = rawText
|
||
.split(/\r?\n/)
|
||
.map((line) => line.trim())
|
||
.filter(Boolean);
|
||
|
||
if (!urls.length) {
|
||
setImportStatus('Keine gültigen Zeilen gefunden.', true);
|
||
return;
|
||
}
|
||
|
||
state.importing = true;
|
||
if (importSubmitBtn) {
|
||
importSubmitBtn.disabled = true;
|
||
}
|
||
setImportStatus('Import läuft...');
|
||
|
||
try {
|
||
const result = await apiFetch(`${API_URL}/daily-bookmarks/import`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ urls, marker, day: state.dayKey })
|
||
});
|
||
|
||
const importedItems = Array.isArray(result && result.items) ? result.items : [];
|
||
const importedIds = new Set(importedItems.map((entry) => entry.id));
|
||
const remaining = state.items.filter((item) => !importedIds.has(item.id));
|
||
state.items = [...importedItems, ...remaining];
|
||
|
||
const summaryParts = [];
|
||
const createdCount = result && typeof result.created === 'number' ? result.created : importedItems.length;
|
||
summaryParts.push(`${createdCount} neu`);
|
||
if (result && result.skipped_existing) summaryParts.push(`${result.skipped_existing} bereits vorhanden`);
|
||
if (result && result.skipped_invalid) summaryParts.push(`${result.skipped_invalid} ungültig`);
|
||
if (result && result.skipped_duplicates) summaryParts.push(`${result.skipped_duplicates} Duplikate`);
|
||
|
||
setListStatus(`Import fertig: ${summaryParts.join(' · ')}`);
|
||
updateHeroStats();
|
||
renderMarkerFilterOptions();
|
||
renderTable();
|
||
closeImportModal();
|
||
} catch (error) {
|
||
setImportStatus(error && error.message ? error.message : 'Import fehlgeschlagen.', true);
|
||
} finally {
|
||
state.importing = false;
|
||
if (importSubmitBtn) {
|
||
importSubmitBtn.disabled = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
function setupEvents() {
|
||
if (prevDayBtn) {
|
||
prevDayBtn.addEventListener('click', () => setDayKey(formatDayKey(addDays(parseDayKey(state.dayKey), -1))));
|
||
}
|
||
if (nextDayBtn) {
|
||
nextDayBtn.addEventListener('click', () => setDayKey(formatDayKey(addDays(parseDayKey(state.dayKey), 1))));
|
||
}
|
||
if (todayBtn) {
|
||
todayBtn.addEventListener('click', () => setDayKey(formatDayKey(new Date())));
|
||
}
|
||
if (refreshBtn) {
|
||
refreshBtn.addEventListener('click', () => loadDailyBookmarks());
|
||
}
|
||
if (openCreateBtn) {
|
||
openCreateBtn.addEventListener('click', () => openModal('create'));
|
||
}
|
||
if (modalCloseBtn) {
|
||
modalCloseBtn.addEventListener('click', closeModal);
|
||
}
|
||
if (modal) {
|
||
modal.addEventListener('click', (event) => {
|
||
if (event.target === modal) {
|
||
closeModal();
|
||
}
|
||
});
|
||
}
|
||
if (modalBackdrop) {
|
||
modalBackdrop.addEventListener('click', closeModal);
|
||
}
|
||
document.addEventListener('keydown', (event) => {
|
||
if (event.key === 'Escape') {
|
||
if (modal && !modal.hidden) {
|
||
closeModal();
|
||
}
|
||
if (importModal && !importModal.hidden) {
|
||
closeImportModal();
|
||
}
|
||
}
|
||
});
|
||
if (urlInput) {
|
||
urlInput.addEventListener('input', updatePreviewLink);
|
||
urlInput.addEventListener('blur', renderUrlSuggestions);
|
||
}
|
||
if (formEl) {
|
||
formEl.addEventListener('submit', submitForm);
|
||
}
|
||
if (resetBtn) {
|
||
resetBtn.addEventListener('click', resetForm);
|
||
}
|
||
if (bulkCountSelect) {
|
||
bulkCountSelect.value = String(state.bulkCount);
|
||
bulkCountSelect.addEventListener('change', () => {
|
||
const value = parseInt(bulkCountSelect.value, 10);
|
||
if (!Number.isNaN(value)) {
|
||
state.bulkCount = value;
|
||
persistBulkCount(value);
|
||
}
|
||
});
|
||
}
|
||
if (bulkOpenBtn) {
|
||
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 || '';
|
||
persistFilters(state.filters);
|
||
updateHeroStats();
|
||
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: '', url: '' };
|
||
state.sort = { ...DEFAULT_SORT };
|
||
persistFilters(state.filters);
|
||
persistSort(state.sort);
|
||
renderMarkerFilterOptions();
|
||
if (urlFilterInput) {
|
||
urlFilterInput.value = '';
|
||
}
|
||
updateHeroStats();
|
||
renderTable();
|
||
});
|
||
}
|
||
if (sortButtons && sortButtons.length) {
|
||
sortButtons.forEach((btn) => {
|
||
btn.addEventListener('click', () => {
|
||
const key = btn.dataset.sortKey;
|
||
if (!key) return;
|
||
if (state.sort.column === key) {
|
||
state.sort.direction = state.sort.direction === 'asc' ? 'desc' : 'asc';
|
||
} else {
|
||
const defaultDirection = ['last_completed_at', 'updated_at', 'created_at'].includes(key) ? 'desc' : 'asc';
|
||
state.sort = { column: key, direction: defaultDirection };
|
||
}
|
||
persistSort(state.sort);
|
||
renderTable();
|
||
});
|
||
});
|
||
}
|
||
if (openImportBtn) {
|
||
openImportBtn.addEventListener('click', openImportModal);
|
||
}
|
||
if (importCloseBtn) {
|
||
importCloseBtn.addEventListener('click', closeImportModal);
|
||
}
|
||
if (importModal) {
|
||
importModal.addEventListener('click', (event) => {
|
||
if (event.target === importModal) {
|
||
closeImportModal();
|
||
}
|
||
});
|
||
}
|
||
if (importBackdrop) {
|
||
importBackdrop.addEventListener('click', closeImportModal);
|
||
}
|
||
if (importForm) {
|
||
importForm.addEventListener('submit', submitImportForm);
|
||
}
|
||
if (importResetBtn) {
|
||
importResetBtn.addEventListener('click', resetImportForm);
|
||
}
|
||
}
|
||
|
||
function init() {
|
||
updateDayUI();
|
||
setupEvents();
|
||
loadDailyBookmarks();
|
||
updatePreviewLink();
|
||
}
|
||
|
||
init();
|
||
})();
|