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

@@ -2759,7 +2759,6 @@ app.post('/api/daily-bookmarks/import', (req, res) => {
notes: '', notes: '',
marker: normalizedMarker marker: normalizedMarker
}); });
upsertDailyBookmarkCheckStmt.run({ bookmarkId: id, dayKey });
const saved = getDailyBookmarkStmt.get({ bookmarkId: id, dayKey }); const saved = getDailyBookmarkStmt.get({ bookmarkId: id, dayKey });
if (saved) { if (saved) {
createdItems.push(saved); createdItems.push(saved);

98
package-lock.json generated Normal file
View File

@@ -0,0 +1,98 @@
{
"name": "fb",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@babel/parser": "^7.28.5",
"acorn": "^8.15.0",
"acorn-loose": "^8.5.2",
"esprima": "^4.0.1"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.5"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-loose": {
"version": "8.5.2",
"resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.5.2.tgz",
"integrity": "sha512-PPvV6g8UGMGgjrMu+n/f9E/tCSkNQ2Y97eFvuVdJfG11+xdIeDcLyNdC8SHcrHbRqkfwLASdplyR6B6sKM1U4A==",
"license": "MIT",
"dependencies": {
"acorn": "^8.15.0"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
}
}
}

8
package.json Normal file
View File

@@ -0,0 +1,8 @@
{
"dependencies": {
"@babel/parser": "^7.28.5",
"acorn": "^8.15.0",
"acorn-loose": "^8.5.2",
"esprima": "^4.0.1"
}
}

View File

@@ -120,6 +120,87 @@ a:hover {
border: 1px solid rgba(37, 99, 235, 0.15); border: 1px solid rgba(37, 99, 235, 0.15);
} }
.auto-open-overlay {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background:
radial-gradient(circle at 20% 20%, rgba(37, 99, 235, 0.12), transparent 42%),
radial-gradient(circle at 80% 30%, rgba(6, 182, 212, 0.12), transparent 38%),
rgba(15, 23, 42, 0.6);
z-index: 30;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease;
}
.auto-open-overlay.visible {
opacity: 1;
pointer-events: auto;
}
.auto-open-overlay__panel {
width: min(940px, 100%);
background: linear-gradient(150deg, rgba(255, 255, 255, 0.9), rgba(248, 250, 252, 0.96));
border-radius: 22px;
padding: 38px 42px 40px;
box-shadow: 0 32px 90px rgba(15, 23, 42, 0.4);
border: 1px solid rgba(255, 255, 255, 0.6);
text-align: center;
cursor: pointer;
}
.auto-open-overlay__badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 999px;
background: rgba(37, 99, 235, 0.12);
color: #0f172a;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
font-size: 12px;
}
.auto-open-overlay__timer {
display: flex;
align-items: baseline;
justify-content: center;
gap: 12px;
margin: 18px 0 8px;
color: #0f172a;
}
.auto-open-overlay__count {
font-size: clamp(72px, 12vw, 120px);
line-height: 1;
font-weight: 700;
letter-spacing: -0.02em;
}
.auto-open-overlay__unit {
font-size: 22px;
color: var(--muted);
}
.auto-open-overlay__text {
margin: 0 auto;
color: #334155;
max-width: 700px;
font-size: 18px;
}
.auto-open-overlay__hint {
margin: 12px 0 0;
color: #475569;
font-size: 15px;
}
.hero__controls { .hero__controls {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -181,6 +262,19 @@ a:hover {
font-size: 13px; font-size: 13px;
} }
.auto-open-toggle {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--muted);
}
.auto-open-toggle input {
width: 16px;
height: 16px;
}
.bulk-actions select { .bulk-actions select {
background: #fff; background: #fff;
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -477,6 +571,10 @@ a:hover {
width: 180px; width: 180px;
} }
.bookmark-table th.col-created {
width: 160px;
}
.bookmark-table th.col-last { .bookmark-table th.col-last {
width: 220px; width: 220px;
} }
@@ -659,6 +757,15 @@ a:hover {
color: var(--text); color: var(--text);
} }
.table-filter-row input[type="search"] {
width: 100%;
border-radius: 10px;
border: 1px solid var(--border);
padding: 8px 10px;
background: #fff;
color: var(--text);
}
.filter-hint { .filter-hint {
font-size: 12px; font-size: 12px;
color: var(--muted); color: var(--muted);
@@ -666,8 +773,7 @@ a:hover {
letter-spacing: 0; letter-spacing: 0;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; justify-content: center;
justify-content: space-between;
} }
.import-hint { .import-hint {

View File

@@ -35,6 +35,10 @@
<option value="15">15</option> <option value="15">15</option>
<option value="20">20</option> <option value="20">20</option>
</select> </select>
<label class="auto-open-toggle">
<input type="checkbox" id="autoOpenToggle">
<span>Auto öffnen</span>
</label>
<button class="secondary-btn" id="bulkOpenBtn" type="button">Öffnen & abhaken</button> <button class="secondary-btn" id="bulkOpenBtn" type="button">Öffnen & abhaken</button>
</div> </div>
<button class="primary-btn" id="openCreateBtn" type="button">+ Bookmark</button> <button class="primary-btn" id="openCreateBtn" type="button">+ Bookmark</button>
@@ -45,6 +49,20 @@
</div> </div>
</header> </header>
<div id="autoOpenOverlay" class="auto-open-overlay" hidden>
<div class="auto-open-overlay__panel" id="autoOpenOverlayPanel">
<div class="auto-open-overlay__badge">Auto-Öffnen startet gleich</div>
<div class="auto-open-overlay__timer">
<span id="autoOpenCountdown" class="auto-open-overlay__count">0.0</span>
<span class="auto-open-overlay__unit">Sek.</span>
</div>
<p class="auto-open-overlay__text">
Die nächste Runde deiner gefilterten Bookmarks öffnet automatisch. Abbrechen, falls du noch warten willst.
</p>
<p class="auto-open-overlay__hint">Klicke irgendwo in dieses Panel, um abzubrechen.</p>
</div>
</div>
<main class="panel list-panel"> <main class="panel list-panel">
<header class="panel__header panel__header--row"> <header class="panel__header panel__header--row">
<div> <div>
@@ -64,13 +82,19 @@
<th class="col-marker"> <th class="col-marker">
<button type="button" class="sort-btn" data-sort-key="marker">Marker</button> <button type="button" class="sort-btn" data-sort-key="marker">Marker</button>
</th> </th>
<th class="col-created">
<button type="button" class="sort-btn" data-sort-key="created_at">Erstelldatum</button>
</th>
<th class="col-last"> <th class="col-last">
<button type="button" class="sort-btn" data-sort-key="last_completed_at">Letzte Erledigung</button> <button type="button" class="sort-btn" data-sort-key="last_completed_at">Letzte Erledigung</button>
</th> </th>
<th>Aktionen</th> <th>Aktionen</th>
</tr> </tr>
<tr class="table-filter-row"> <tr class="table-filter-row">
<th></th> <th>
<label class="visually-hidden" for="urlFilter">Nach URL filtern</label>
<input id="urlFilter" type="search" placeholder="URL filtern">
</th>
<th> <th>
<label class="visually-hidden" for="markerFilter">Nach Marker filtern</label> <label class="visually-hidden" for="markerFilter">Nach Marker filtern</label>
<select id="markerFilter"> <select id="markerFilter">
@@ -78,9 +102,10 @@
<option value="__none">Ohne Marker</option> <option value="__none">Ohne Marker</option>
</select> </select>
</th> </th>
<th colspan="2" class="filter-hint"> <th></th>
<span>Filter & Sortierung werden gespeichert</span> <th></th>
<button type="button" class="ghost-btn ghost-btn--tiny" id="resetViewBtn">Zurücksetzen</button> <th class="filter-hint">
<button type="button" class="ghost-btn ghost-btn--tiny" id="resetViewBtn" aria-label="Filter und Sortierung zurücksetzen"></button>
</th> </th>
</tr> </tr>
</thead> </thead>

View File

@@ -3,8 +3,10 @@
const BULK_COUNT_STORAGE_KEY = 'dailyBookmarkBulkCount'; const BULK_COUNT_STORAGE_KEY = 'dailyBookmarkBulkCount';
const FILTER_STORAGE_KEY = 'dailyBookmarkFilters'; const FILTER_STORAGE_KEY = 'dailyBookmarkFilters';
const SORT_STORAGE_KEY = 'dailyBookmarkSort'; const SORT_STORAGE_KEY = 'dailyBookmarkSort';
const AUTO_OPEN_STORAGE_KEY = 'dailyBookmarkAutoOpen';
const DEFAULT_BULK_COUNT = 5; 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 = { const state = {
dayKey: formatDayKey(new Date()), dayKey: formatDayKey(new Date()),
@@ -16,7 +18,11 @@
bulkCount: loadBulkCount(), bulkCount: loadBulkCount(),
filters: loadFilters(), filters: loadFilters(),
sort: loadSort(), sort: loadSort(),
importing: false importing: false,
autoOpenEnabled: loadAutoOpenEnabled(),
autoOpenTriggered: false,
autoOpenTimerId: null,
autoOpenCountdownIntervalId: null
}; };
let editingId = null; let editingId = null;
@@ -33,6 +39,10 @@
const tableBody = document.getElementById('tableBody'); const tableBody = document.getElementById('tableBody');
const bulkCountSelect = document.getElementById('bulkCountSelect'); const bulkCountSelect = document.getElementById('bulkCountSelect');
const bulkOpenBtn = document.getElementById('bulkOpenBtn'); 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 openCreateBtn = document.getElementById('openCreateBtn');
const modal = document.getElementById('bookmarkModal'); const modal = document.getElementById('bookmarkModal');
@@ -50,6 +60,7 @@
const urlSuggestionBox = document.getElementById('urlSuggestionBox'); const urlSuggestionBox = document.getElementById('urlSuggestionBox');
const markerInput = document.getElementById('markerInput'); const markerInput = document.getElementById('markerInput');
const markerFilterSelect = document.getElementById('markerFilter'); const markerFilterSelect = document.getElementById('markerFilter');
const urlFilterInput = document.getElementById('urlFilter');
const resetViewBtn = document.getElementById('resetViewBtn'); const resetViewBtn = document.getElementById('resetViewBtn');
const sortButtons = Array.from(document.querySelectorAll('[data-sort-key]')); const sortButtons = Array.from(document.querySelectorAll('[data-sort-key]'));
const openImportBtn = document.getElementById('openImportBtn'); const openImportBtn = document.getElementById('openImportBtn');
@@ -140,13 +151,14 @@
if (raw) { if (raw) {
const parsed = JSON.parse(raw); const parsed = JSON.parse(raw);
return { return {
marker: typeof parsed.marker === 'string' ? parsed.marker : '' marker: typeof parsed.marker === 'string' ? parsed.marker : '',
url: typeof parsed.url === 'string' ? parsed.url : ''
}; };
} }
} catch (error) { } catch (error) {
// ignore // ignore
} }
return { marker: '' }; return { marker: '', url: '' };
} }
function persistFilters(filters) { function persistFilters(filters) {
@@ -162,7 +174,7 @@
const raw = localStorage.getItem(SORT_STORAGE_KEY); const raw = localStorage.getItem(SORT_STORAGE_KEY);
if (raw) { if (raw) {
const parsed = JSON.parse(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']; const allowedDirections = ['asc', 'desc'];
if ( if (
parsed && 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) { function resolveTemplate(template, dayKey) {
if (typeof template !== 'string') { if (typeof template !== 'string') {
return ''; return '';
@@ -524,19 +552,44 @@
return `Vor ${diffDays} Tagen`; 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) { function normalizeMarkerValue(value) {
return (value || '').trim(); return (value || '').trim();
} }
function getFilteredItems() { function getFilteredItems() {
const markerFilter = state.filters.marker || ''; const markerFilter = state.filters.marker || '';
const urlFilter = (state.filters.url || '').toLowerCase();
return state.items.filter((item) => { return state.items.filter((item) => {
const currentMarker = normalizeMarkerValue(item.marker).toLowerCase(); const currentMarker = normalizeMarkerValue(item.marker).toLowerCase();
if (markerFilter === '__none') { if (markerFilter === '__none') {
return !currentMarker; if (currentMarker) {
return false;
}
} else if (markerFilter) {
if (currentMarker !== markerFilter.toLowerCase()) {
return false;
}
} }
if (markerFilter) { if (urlFilter) {
return currentMarker === markerFilter.toLowerCase(); const urlValue = (item.resolved_url || item.url_template || '').toLowerCase();
if (!urlValue.includes(urlFilter)) {
return false;
}
} }
return true; return true;
}); });
@@ -557,6 +610,8 @@
return a.url_template.localeCompare(b.url_template, 'de', { sensitivity: 'base' }) * direction; return a.url_template.localeCompare(b.url_template, 'de', { sensitivity: 'base' }) * direction;
case 'marker': case 'marker':
return normalizeMarkerValue(a.marker).localeCompare(normalizeMarkerValue(b.marker), 'de', { sensitivity: 'base' }) * direction; 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': case 'updated_at':
return (safeTime(a.updated_at) - safeTime(b.updated_at)) * direction; return (safeTime(a.updated_at) - safeTime(b.updated_at)) * direction;
case 'last_completed_at': case 'last_completed_at':
@@ -678,6 +733,10 @@
} }
tr.appendChild(markerTd); tr.appendChild(markerTd);
const createdTd = document.createElement('td');
createdTd.textContent = formatDateStamp(item.created_at);
tr.appendChild(createdTd);
const timeTd = document.createElement('td'); const timeTd = document.createElement('td');
timeTd.textContent = formatRelativeTimestamp(item.last_completed_at); timeTd.textContent = formatRelativeTimestamp(item.last_completed_at);
tr.appendChild(timeTd); tr.appendChild(timeTd);
@@ -740,7 +799,7 @@
function appendSingleRow(text, className) { function appendSingleRow(text, className) {
const tr = document.createElement('tr'); const tr = document.createElement('tr');
const td = document.createElement('td'); const td = document.createElement('td');
td.colSpan = 4; td.colSpan = 5;
td.className = className; td.className = className;
td.textContent = text; td.textContent = text;
tr.appendChild(td); tr.appendChild(td);
@@ -754,21 +813,121 @@
const visibleDone = visibleItems.filter((item) => item.completed_for_day).length; const visibleDone = visibleItems.filter((item) => item.completed_for_day).length;
const filterSuffix = visibleItems.length !== total ? ` · Gefiltert: ${visibleDone}/${visibleItems.length}` : ''; const filterSuffix = visibleItems.length !== total ? ` · Gefiltert: ${visibleDone}/${visibleItems.length}` : '';
const text = total ? `${done}/${total} erledigt${filterSuffix}` : 'Keine Bookmarks vorhanden'; 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) { if (heroStats) {
heroStats.textContent = text; heroStats.textContent = `${text}${filterText}`;
} }
if (listSummary) { if (listSummary) {
const markerInfo = state.filters.marker listSummary.textContent = `${text} Tag ${state.dayKey}${filterText}`;
? state.filters.marker === '__none'
? ' · Filter: ohne Marker'
: ` · Filter: ${state.filters.marker}`
: '';
listSummary.textContent = `${text} Tag ${state.dayKey}${markerInfo}`;
} }
} }
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() { async function loadDailyBookmarks() {
state.loading = true; state.loading = true;
if (state.autoOpenTimerId) {
clearTimeout(state.autoOpenTimerId);
state.autoOpenTimerId = null;
}
hideAutoOpenOverlay();
state.autoOpenTriggered = false;
state.error = ''; state.error = '';
setListStatus(''); setListStatus('');
renderTable(); renderTable();
@@ -779,6 +938,7 @@
updateHeroStats(); updateHeroStats();
renderMarkerFilterOptions(); renderMarkerFilterOptions();
renderTable(); renderTable();
maybeAutoOpen('load');
} catch (error) { } catch (error) {
state.loading = false; state.loading = false;
state.error = 'Konnte Bookmarks nicht laden.'; state.error = 'Konnte Bookmarks nicht laden.';
@@ -897,11 +1057,16 @@
} }
} }
async function openBatch() { async function openBatch({ auto = false } = {}) {
if (state.processingBatch) return; if (state.processingBatch) return;
if (!auto) {
cancelAutoOpen(false);
}
const undone = getVisibleItems().filter((item) => !item.completed_for_day); const undone = getVisibleItems().filter((item) => !item.completed_for_day);
if (!undone.length) { 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; return;
} }
@@ -909,8 +1074,14 @@
const selection = undone.slice(0, count); const selection = undone.slice(0, count);
state.processingBatch = true; state.processingBatch = true;
bulkOpenBtn.disabled = true; if (bulkOpenBtn) {
setListStatus(''); bulkOpenBtn.disabled = true;
}
if (!auto) {
setListStatus('');
} else {
setListStatus(`Öffne automatisch ${selection.length} Links...`, false);
}
for (const item of selection) { for (const item of selection) {
const target = item.resolved_url || item.url_template; const target = item.resolved_url || item.url_template;
@@ -929,7 +1100,12 @@
} }
state.processingBatch = false; state.processingBatch = false;
bulkOpenBtn.disabled = false; if (bulkOpenBtn) {
bulkOpenBtn.disabled = false;
}
if (auto) {
setListStatus('');
}
updateHeroStats(); updateHeroStats();
renderTable(); renderTable();
} }
@@ -1097,8 +1273,38 @@
}); });
} }
if (bulkOpenBtn) { 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) { if (markerFilterSelect) {
markerFilterSelect.addEventListener('change', () => { markerFilterSelect.addEventListener('change', () => {
state.filters.marker = markerFilterSelect.value || ''; state.filters.marker = markerFilterSelect.value || '';
@@ -1107,13 +1313,25 @@
renderTable(); renderTable();
}); });
} }
if (urlFilterInput) {
urlFilterInput.value = state.filters.url || '';
urlFilterInput.addEventListener('input', () => {
state.filters.url = urlFilterInput.value.trim();
persistFilters(state.filters);
updateHeroStats();
renderTable();
});
}
if (resetViewBtn) { if (resetViewBtn) {
resetViewBtn.addEventListener('click', () => { resetViewBtn.addEventListener('click', () => {
state.filters = { marker: '' }; state.filters = { marker: '', url: '' };
state.sort = { ...DEFAULT_SORT }; state.sort = { ...DEFAULT_SORT };
persistFilters(state.filters); persistFilters(state.filters);
persistSort(state.sort); persistSort(state.sort);
renderMarkerFilterOptions(); renderMarkerFilterOptions();
if (urlFilterInput) {
urlFilterInput.value = '';
}
updateHeroStats(); updateHeroStats();
renderTable(); renderTable();
}); });
@@ -1126,7 +1344,7 @@
if (state.sort.column === key) { if (state.sort.column === key) {
state.sort.direction = state.sort.direction === 'asc' ? 'desc' : 'asc'; state.sort.direction = state.sort.direction === 'asc' ? 'desc' : 'asc';
} else { } 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 }; state.sort = { column: key, direction: defaultDirection };
} }
persistSort(state.sort); persistSort(state.sort);