bookmarks restyling

This commit is contained in:
MDeeApp
2025-10-23 21:22:41 +02:00
parent cd5a179125
commit 9d85044b7f
3 changed files with 736 additions and 139 deletions

View File

@@ -24,6 +24,8 @@ const SEARCH_POST_HIDE_THRESHOLD = 2;
const SEARCH_POST_RETENTION_DAYS = 90;
const MAX_POST_TEXT_LENGTH = 4000;
const MIN_TEXT_HASH_LENGTH = 120;
const MAX_BOOKMARK_LABEL_LENGTH = 120;
const MAX_BOOKMARK_QUERY_LENGTH = 200;
const screenshotDir = path.join(__dirname, 'data', 'screenshots');
if (!fs.existsSync(screenshotDir)) {
@@ -316,6 +318,48 @@ function computePostTextHash(text) {
return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
}
function normalizeBookmarkQuery(value) {
if (typeof value !== 'string') {
return null;
}
let query = value.trim();
if (!query) {
return null;
}
query = query.replace(/\s+/g, ' ');
if (query.length > MAX_BOOKMARK_QUERY_LENGTH) {
query = query.slice(0, MAX_BOOKMARK_QUERY_LENGTH);
}
return query;
}
function normalizeBookmarkLabel(value, fallback = '') {
const base = typeof value === 'string' ? value.trim() : '';
let label = base || fallback || '';
label = label.replace(/\s+/g, ' ');
if (label.length > MAX_BOOKMARK_LABEL_LENGTH) {
label = label.slice(0, MAX_BOOKMARK_LABEL_LENGTH);
}
return label;
}
function serializeBookmark(row) {
if (!row) {
return null;
}
return {
id: row.id,
label: row.label,
query: row.query,
created_at: sqliteTimestampToUTC(row.created_at),
updated_at: sqliteTimestampToUTC(row.updated_at),
last_clicked_at: sqliteTimestampToUTC(row.last_clicked_at)
};
}
function normalizeFacebookPostUrl(rawValue) {
if (typeof rawValue !== 'string') {
return null;
@@ -655,6 +699,66 @@ db.exec(`
ON search_seen_posts(last_seen_at);
`);
db.exec(`
CREATE TABLE IF NOT EXISTS bookmarks (
id TEXT PRIMARY KEY,
label TEXT,
query TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_clicked_at DATETIME,
UNIQUE(query COLLATE NOCASE)
);
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_bookmarks_last_clicked_at
ON bookmarks(last_clicked_at);
`);
db.exec(`
CREATE INDEX IF NOT EXISTS idx_bookmarks_created_at
ON bookmarks(created_at);
`);
const listBookmarksStmt = db.prepare(`
SELECT id, label, query, created_at, updated_at, last_clicked_at
FROM bookmarks
ORDER BY
(last_clicked_at IS NULL),
datetime(COALESCE(last_clicked_at, created_at)) DESC,
label COLLATE NOCASE
`);
const getBookmarkByIdStmt = db.prepare(`
SELECT id, label, query, created_at, updated_at, last_clicked_at
FROM bookmarks
WHERE id = ?
`);
const findBookmarkByQueryStmt = db.prepare(`
SELECT id
FROM bookmarks
WHERE LOWER(query) = LOWER(?)
`);
const insertBookmarkStmt = db.prepare(`
INSERT INTO bookmarks (id, label, query)
VALUES (?, ?, ?)
`);
const deleteBookmarkStmt = db.prepare(`
DELETE FROM bookmarks
WHERE id = ?
`);
const updateBookmarkLastClickedStmt = db.prepare(`
UPDATE bookmarks
SET last_clicked_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`);
ensureColumn('posts', 'checked_count', 'checked_count INTEGER DEFAULT 0');
ensureColumn('posts', 'screenshot_path', 'screenshot_path TEXT');
ensureColumn('posts', 'created_by_profile', 'created_by_profile INTEGER');
@@ -1693,6 +1797,79 @@ function mapPostRow(post) {
};
}
app.get('/api/bookmarks', (req, res) => {
try {
const rows = listBookmarksStmt.all();
res.json(rows.map(serializeBookmark));
} catch (error) {
console.error('Failed to load bookmarks:', error);
res.status(500).json({ error: 'Bookmarks konnten nicht geladen werden' });
}
});
app.post('/api/bookmarks', (req, res) => {
try {
const payload = req.body || {};
const normalizedQuery = normalizeBookmarkQuery(payload.query);
if (!normalizedQuery) {
return res.status(400).json({ error: 'Ungültiger Suchbegriff' });
}
if (findBookmarkByQueryStmt.get(normalizedQuery)) {
return res.status(409).json({ error: 'Bookmark existiert bereits' });
}
const normalizedLabel = normalizeBookmarkLabel(payload.label, normalizedQuery);
const id = uuidv4();
insertBookmarkStmt.run(id, normalizedLabel, normalizedQuery);
const saved = getBookmarkByIdStmt.get(id);
res.status(201).json(serializeBookmark(saved));
} catch (error) {
console.error('Failed to create bookmark:', error);
res.status(500).json({ error: 'Bookmark konnte nicht erstellt werden' });
}
});
app.post('/api/bookmarks/:bookmarkId/click', (req, res) => {
const { bookmarkId } = req.params;
if (!bookmarkId) {
return res.status(400).json({ error: 'Bookmark-ID fehlt' });
}
try {
const result = updateBookmarkLastClickedStmt.run(bookmarkId);
if (!result.changes) {
return res.status(404).json({ error: 'Bookmark nicht gefunden' });
}
const updated = getBookmarkByIdStmt.get(bookmarkId);
res.json(serializeBookmark(updated));
} catch (error) {
console.error('Failed to register bookmark click:', error);
res.status(500).json({ error: 'Bookmark konnte nicht aktualisiert werden' });
}
});
app.delete('/api/bookmarks/:bookmarkId', (req, res) => {
const { bookmarkId } = req.params;
if (!bookmarkId) {
return res.status(400).json({ error: 'Bookmark-ID fehlt' });
}
try {
const result = deleteBookmarkStmt.run(bookmarkId);
if (!result.changes) {
return res.status(404).json({ error: 'Bookmark nicht gefunden' });
}
res.status(204).send();
} catch (error) {
console.error('Failed to delete bookmark:', error);
res.status(500).json({ error: 'Bookmark konnte nicht gelöscht werden' });
}
});
// Get all posts
app.get('/api/posts', (req, res) => {
try {