daily bookmarks
This commit is contained in:
@@ -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
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);
|
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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user