daily bookmarks
This commit is contained in:
@@ -2759,7 +2759,6 @@ app.post('/api/daily-bookmarks/import', (req, res) => {
|
||||
notes: '',
|
||||
marker: normalizedMarker
|
||||
});
|
||||
upsertDailyBookmarkCheckStmt.run({ bookmarkId: id, dayKey });
|
||||
const saved = getDailyBookmarkStmt.get({ bookmarkId: id, dayKey });
|
||||
if (saved) {
|
||||
createdItems.push(saved);
|
||||
|
||||
98
package-lock.json
generated
Normal file
98
package-lock.json
generated
Normal 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
8
package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-loose": "^8.5.2",
|
||||
"esprima": "^4.0.1"
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,87 @@ a:hover {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -181,6 +262,19 @@ a:hover {
|
||||
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 {
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
@@ -477,6 +571,10 @@ a:hover {
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.bookmark-table th.col-created {
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.bookmark-table th.col-last {
|
||||
width: 220px;
|
||||
}
|
||||
@@ -659,6 +757,15 @@ a:hover {
|
||||
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 {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
@@ -666,8 +773,7 @@ a:hover {
|
||||
letter-spacing: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.import-hint {
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
<option value="15">15</option>
|
||||
<option value="20">20</option>
|
||||
</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>
|
||||
</div>
|
||||
<button class="primary-btn" id="openCreateBtn" type="button">+ Bookmark</button>
|
||||
@@ -45,6 +49,20 @@
|
||||
</div>
|
||||
</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">
|
||||
<header class="panel__header panel__header--row">
|
||||
<div>
|
||||
@@ -64,13 +82,19 @@
|
||||
<th class="col-marker">
|
||||
<button type="button" class="sort-btn" data-sort-key="marker">Marker</button>
|
||||
</th>
|
||||
<th class="col-created">
|
||||
<button type="button" class="sort-btn" data-sort-key="created_at">Erstelldatum</button>
|
||||
</th>
|
||||
<th class="col-last">
|
||||
<button type="button" class="sort-btn" data-sort-key="last_completed_at">Letzte Erledigung</button>
|
||||
</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
<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>
|
||||
<label class="visually-hidden" for="markerFilter">Nach Marker filtern</label>
|
||||
<select id="markerFilter">
|
||||
@@ -78,9 +102,10 @@
|
||||
<option value="__none">Ohne Marker</option>
|
||||
</select>
|
||||
</th>
|
||||
<th colspan="2" class="filter-hint">
|
||||
<span>Filter & Sortierung werden gespeichert</span>
|
||||
<button type="button" class="ghost-btn ghost-btn--tiny" id="resetViewBtn">Zurücksetzen</button>
|
||||
<th></th>
|
||||
<th></th>
|
||||
<th class="filter-hint">
|
||||
<button type="button" class="ghost-btn ghost-btn--tiny" id="resetViewBtn" aria-label="Filter und Sortierung zurücksetzen">↺</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user